本文目录导读:

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机制本身没有根本性改变。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播