PHP垃圾回收机制了解吗

wen PHP项目 2

本文目录导读:

PHP垃圾回收机制了解吗

  1. 核心概念:引用计数
  2. 旧版GC的缺陷:循环引用(PHP < 5.3)
  3. 现代GC:循环引用收集器(PHP >= 5.3)
  4. 两种GC模式的共存
  5. 性能影响与调优
  6. 代码演示
  7. 总结与建议

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》)。

工作原理:

  1. 根缓冲区:当一个变量的 refcount 减少但未变为0时(即可能被引用),PHP会把它放入一个“根缓冲区”列表。
  2. 触发条件:当根缓冲区满(默认10000个节点)或调用 gc_collect_cycles() 时,GC开始工作。
  3. 算法步骤
    • 标记(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"; // 内存大幅下降

总结与建议

  1. 原理:基于引用计数 + 循环引用检测算法。
  2. 关键区分refcount=0 立即回收(快),循环引用延迟回收(需扫描)。
  3. 对开发者的意义
    • 不要依赖 unset() 来强制内存释放(除非有循环引用才用到GC)。
    • 长生命周期应用(如常驻内存的框架、Swoole、ReactPHP)必须开启GC
    • 在循环中创建大量对象/数组时,考虑用 unset() 配合 gc_collect_cycles() 及时清理。
  4. 版本差异:PHP 7+ 性能远超 PHP 5.x,主要因为标量不计数。

PHP 8.x 继承了PHP 7的设计,但引入了 JIT(Just-In-Time)编译器,进一步提高了CPU密集型任务性能,但GC机制本身没有根本性改变。

抱歉,评论功能暂时关闭!