PHP Academy · 课时

内存管理与垃圾回收

了解 zval、引用计数和垃圾回收器的工作方式

第 2 / 4 课13 个步骤

内存管理与垃圾回收 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。

PHP 如何管理内存

PHP 值存储在 zval 容器中,主要通过引用计数管理,并通过循环收集器处理引用计数无法处理的情况。理解这一点有助于您理解长时间运行的工作进程中的内存泄漏、复制的开销,以及何时需要使用 gc_collect_cycles()。

zval 容器

zval 是任何 PHP 值的容器:其中包含类型标签和用于存储值的联合体。标量(整数、浮点数和布尔值)在 zval 内部按值存储,并且 NOT 参与引用计数。复杂类型(字符串、数组和对象)则指向堆上的引用计数结构。

<?php
// debug_zval_refcount-style inspection via gc + count
$arr = [1, 2, 3];
$copy = $arr;   // refcount of the array bumps, no data copy yet (CoW)

// Modifying triggers copy-on-write separation
$copy[] = 4;

var_dump($arr);   // still [1,2,3]
var_dump($copy);  // [1,2,3,4]
?>

引用计数

每个采用引用计数的值都会记录指向它的 zval 数量。赋值会增加计数;取消设置变量或离开作用域会减少计数。当计数达到零时,内存会立即释放——无需等待垃圾收集轮次。PHP 的大部分内存都通过这种方式回收。

<?php
$a = str_repeat('x', 1000);  // refcount 1 on the string
$b = $a;                      // refcount 2
unset($a);                    // refcount 1
unset($b);                    // refcount 0 -> freed instantly

echo "Refcounting frees deterministically\n";
?>

写时复制

赋值不会复制数据,而是共享数据并增加引用计数(写时复制)。只有在首次写入共享值时,才会真正进行复制。因此,在修改大型数组之前,传递它们的成本很低。

<?php
$big = range(1, 100000);
$shared = $big;          // O(1): shared, refcount++

// Reads stay shared and cheap
echo count($shared), PHP_EOL;

// First write separates (copies) — now it costs O(n)
$shared[0] = -1;
echo $big[0], ' vs ', $shared[0], PHP_EOL;  // 1 vs -1
?>

问题:循环引用

仅靠引用计数无法释放引用循环。如果对象 A 持有对 B 的引用,而 B 又反过来持有对 A 的引用,那么即使您取消设置所有外部变量,它们的计数也永远不会降到零——直到循环收集器运行之前,这部分内存都会泄漏。

<?php
class Node { public ?Node $ref = null; }

$a = new Node();
$b = new Node();
$a->ref = $b;   // a -> b
$b->ref = $a;   // b -> a  (cycle!)

unset($a, $b);  // external refs gone, but internal cycle keeps refcount > 0

// The objects are unreachable but NOT yet freed by refcounting alone.
echo gc_collect_cycles() . " cycles collected\n";
?>

循环收集器

PHP 的垃圾收集器实现了针对根的同步标记-清除算法。潜在的循环根(引用计数减少但未降为零的值)会被放入缓冲区。当根缓冲区填满时(默认容量为 10,000),垃圾收集器会运行:它会模拟引用计数递减,找出真正不可达的对象组并释放它们。

<?php
// GC status exposes the root buffer and run stats
for ($i = 0; $i < 3; $i++) {
    $a = new stdClass();
    $b = new stdClass();
    $a->b = $b; $b->a = $a;   // create a cycle
    unset($a, $b);            // becomes a GC root
}

print_r(gc_status());
?>

控制垃圾回收

您可以控制收集器:gc_disable()/gc_enable() 用于切换其状态,gc_collect_cycles() 会强制执行一次回收并返回释放的数量。在吞吐量关键的批处理任务中,有时会在紧密执行阶段禁用垃圾回收,然后在末尾统一回收一次。

<?php
gc_disable();              // no automatic cycle collection

// ... heavy phase that creates and drops many cycles ...
for ($i = 0; $i < 1000; $i++) {
    $x = new stdClass(); $y = new stdClass();
    $x->y = $y; $y->x = $x; unset($x, $y);
}

$freed = gc_collect_cycles();   // reclaim all at once
gc_enable();
echo "Freed $freed objects\n";
?>

长时间运行的工作进程

在 CLI 守护进程、队列消费者以及 Swoole/RoadRunner 工作进程中,内存不会通过请求结束流程回收,因为进程会持续运行数小时。请注意以下问题:无限增长的静态缓存、不断累积的循环引用,以及未关闭的资源。请使用 memory_get_usage(true) 监控内存,并定期重启工作进程。

<?php
$peakStart = memory_get_usage(true);

// Simulate processing a batch of jobs
for ($job = 0; $job < 5; $job++) {
    $payload = range(0, 10000);
    // ... process ...
    unset($payload);
}

printf("start: %d KB, peak: %d KB, now: %d KB\n",
    $peakStart >> 10,
    memory_get_peak_usage(true) >> 10,
    memory_get_usage(true) >> 10
);
?>

WeakReference 与 WeakMap

如果您想缓存对象相关的数据,或将数据与对象关联起来,同时又不让对象保持存活,请使用 WeakReference(8.0)和 WeakMap(8.0)。WeakMap 条目不会增加键对象的引用计数,因此对象可以被回收,其条目也会自动移除——这非常适合用于不能造成泄漏的元数据辅助表。

<?php
$map = new WeakMap();

$obj = new stdClass();
$map[$obj] = 'metadata';      // does NOT keep $obj alive

echo count($map) . PHP_EOL;   // 1
unset($obj);                  // object collectible -> entry removed
echo count($map) . PHP_EOL;   // 0
?>

驻留字符串与不可变值

源代码中的字符串字面量是驻留的:它们只存储一次,可以共享,并且在请求期间永远不会参与引用计数或被释放。OPcache 会将驻留字符串和不可变的(已缓存的)op_arrays 移入共享内存,使其能够跨请求共享。因此,复制字符串字面量的成本实际上为零。

内存限制与分配器

PHP 使用自己的 emalloc 分配器(Zend 内存管理器),构建在系统分配器之上,并根据 memory_limit 跟踪每次请求的内存使用量。达到限制时会抛出致命错误。true 位于 memory_get_usage(true) 中时,会报告 OS 实际分配的内存块,而不仅仅是脚本当前持有的内存。

<?php
echo 'limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'emalloc usage: ' . memory_get_usage() . " bytes\n";   // ZendMM tracked
echo 'real usage:    ' . memory_get_usage(true) . " bytes\n"; // from OS
?>

快速检查

为什么仅靠引用计数会导致某些对象泄漏?

回顾

PHP 将值存储在 zval 容器中;标量按值存储,复杂类型在堆上分配并采用引用计数管理,同时支持写时复制。引用计数在计数为零时能够确定性地释放内存,但无法回收循环引用,因此同步标记-清除收集器会在根缓冲区填满时运行(也可以通过 gc_collect_cycles() 运行)。请使用 WeakMap/WeakReference 建立非拥有式关联,并留意长时间运行的工作进程是否出现无限增长。

免费开始

用 AI 导师学习 PHP — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
49
课程
195

常见问题解答

「内存管理与垃圾回收」课时是免费的吗?

是的 — 「内存管理与垃圾回收」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。

「内存管理与垃圾回收」这节课中我会学到什么?

了解 zval、引用计数和垃圾回收器的工作方式 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 PHP Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「内存管理与垃圾回收」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 PHP Academy 课中编写并运行代码吗?

能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. Zend 引擎的工作原理
  2. 内存管理与垃圾回收
  3. OPcache 与 JIT 编译
  4. 使用 C 编写基础 PHP 扩展
← 返回 PHP Academy