本文目录导读:

PHP的垃圾回收(Garbage Collection,简称GC)机制是PHP内存管理的核心,它的主要目标是自动回收不再使用的内存,防止内存泄漏。
要了解PHP的GC,需要区分PHP 5.3之前和PHP 5.3之后(含)两个重要阶段,因为这是分水岭。
核心概念:引用计数
PHP的变量存储在一个叫 zval 的结构体中,GC的基础是引用计数。
- 每个变量(zval)都有一个
refcount(引用计数)。 - 当变量被赋值给另一个变量时,
refcount加1(写时复制除外)。 - 当变量被
unset()或离开作用域时,refcount减1。 - 当
refcount变为0时,该变量占用的内存会被立即释放。
这是最基础的GC机制,但不完美。
旧版GC的缺陷:循环引用(PHP < 5.3)
在PHP 5.3之前,只有引用计数,没有循环引用检测。
问题场景:
$a = ['value' => 1]; $a['self'] = &$a; // 数组自己引用自己 unset($a); // $a 的 refcount 变为 1(因为内部 self 还指向它),永远不会变成 0
结果: 这块内存永远不会被释放,这就是内存泄漏,这在面向对象编程(对象互相引用)中很常见。
现代GC:循环引用收集器(PHP >= 5.3)
为了解决循环引用问题,PHP引入了根缓冲区和GC算法(基于论文《Concurrent Cycle Collection in Reference Counted Systems》)。
工作原理:
- 根缓冲区:当一个变量的
refcount减少但未变为0时(即可能被引用),PHP会把它放入一个“根缓冲区”列表。 - 触发条件:当根缓冲区满(默认10000个节点)或调用
gc_collect_cycles()时,GC开始工作。 - 算法步骤:
- 标记(Mark):将根缓冲区中的所有变量当做根节点,遍历其所有子节点,将每个节点的引用计数临时减1(模拟删除根节点)。
- 扫描(Scan):再次遍历,如果某个节点在减1后引用计数变为0,说明它是一个孤立的循环(没有被外部引用),将其标记为“可疑”。
- 清理(Sweep):将所有“可疑”的节点引用计数恢复原值,并释放它们的内存,改变量从缓冲区移除。
图示流程:
$a = ['x' => 1];
$a['y'] = &$a; // 循环引用
unset($a); // refcount = 1 (内部引用), 进入根缓冲区
// 缓冲区满了:
// 1. 步骤一:模拟减去所有引用的计数 -> 临时refcount = 0
// 2. 步骤二:发现 refcount == 0,标记为垃圾
// 3. 步骤三:彻底释放内存
两种GC模式的共存
PHP(5.3+)实际上有两套GC:
| 模式 | 机制 | 触发时机 | 适用场景 |
|---|---|---|---|
| 即时GC(引用计数) | 当 refcount 归0时立即释放 |
变量 unset 或作用域结束 |
绝大多数普通变量(无循环引用) |
| 周期GC(循环检测) | 扫描根缓冲区 | 缓冲区满(默认10000)或手动调用 | 仅针对可能存在的循环引用(对象、数组) |
这种设计保证了性能:平时不扫描,只在特定条件下进行深度检测。
性能影响与调优
虽然GC解决了内存泄漏,但扫描过程是耗时的。
PHP 7.x 的重大改进:
- zval 结构优化:PHP 7 将
refcount从单独的结构体(zval_value)移到了引用结构体(zend_reference)中。 - 性能提升:不是所有值都有引用计数,整数、浮点数、布尔值、NULL 等标量类型不开辟引用计数,速度得到大幅提升,只有数组、对象、字符串(长字符串)才参与引用计数。
- 减少GC压力:由于大部分简单变量不再计数,根缓冲区容量增大,GC扫描的频率显著降低。
配置指令(php.ini):
zend.enable_gc:开关循环引用GC(默认On)。- 建议:日常生产环境开启,除非你确定代码中完全没有循环赋值且对性能极致敏感。
手动收集:
- 在批量操作大量对象后(如循环创建对象),使用
gc_collect_cycles()手动触发,能尽快释放内存,避免GC在高峰期自动运行导致卡顿。
代码演示
// 1. 设置GC root buffer 为极小值,以便快速触发
gc_disable(); // 先关闭,便于观察
ini_set('zend.enable_gc', '0');
$objects = [];
for ($i = 0; $i < 100; $i++) {
$obj = new stdClass();
$obj->self = $obj; // 循环引用
$objects[] = $obj;
}
unset($objects);
echo "GC未开启时的内存: " . memory_get_usage() . "\n"; // 内存被占着
// 2. 开启GC并手动收集
gc_enable();
$collected = gc_collect_cycles();
echo "回收了 " . $collected . " 个循环引用\n";
echo "GC开启后的内存: " . memory_get_usage() . "\n"; // 内存大幅下降
总结与建议
- 原理:基于引用计数 + 循环引用检测算法。
- 关键区分:
refcount=0立即回收(快),循环引用延迟回收(需扫描)。 - 对开发者的意义:
- 不要依赖
unset()来强制内存释放(除非有循环引用才用到GC)。 - 长生命周期应用(如常驻内存的框架、Swoole、ReactPHP)必须开启GC。
- 在循环中创建大量对象/数组时,考虑用
unset()配合gc_collect_cycles()及时清理。
- 不要依赖
- 版本差异:PHP 7+ 性能远超 PHP 5.x,主要因为标量不计数。
PHP 8.x 继承了PHP 7的设计,但引入了 JIT(Just-In-Time)编译器,进一步提高了CPU密集型任务性能,但GC机制本身没有根本性改变。