← 返回首页

PHP 的垃圾回收机制是如何工作的

一句话结论

引用计数为主、循环引用收集器为辅,二者协同避免内存泄漏。

体系讲解

原理:为什么需要两层机制

PHP 变量底层以 zval 容器存储,每个 zval 维护一个引用计数(refcount)。当变量的 refcount 降为 0 时,内存立即释放——这是第一层,覆盖绝大多数场景。

但引用计数有一个已知缺陷:无法处理循环引用。当对象 A 持有对 B 的引用、B 同时持有对 A 的引用,即使外部已无变量指向它们,两者的 refcount 仍为 1,永远无法降至 0,内存也就永远无法释放。这就需要第二层:循环引用收集器(Cycle Collector)。

机制:循环引用收集器如何工作

PHP 5.3 起引入的循环引用收集器,基于 David F. Bacon 与 V.T. Rajan 2001 年论文 Concurrent Cycle Collection in Reference Counted Systems 的同步变体。

以下算法描述来源于 PHP 官方文档 Collecting Cycles。

核心流程分四步:

  1. 收集疑似根(Purple):当 zval 的 refcount 减少但未降至 0 时,该 zval 被标记为"疑似垃圾根"放入根缓冲区(root buffer)。根缓冲区固定容量为 10,000 个根节点(源码中的 GC_THRESHOLD_DEFAULT 常量,可重新编译修改)。

  2. 模拟删除(Mark Grey):根缓冲区满时,收集算法启动。从每个疑似根出发做深度优先遍历,对所引用的每个 zval 执行 refcount−1(模拟移除引用),并标记为灰色。同一 zval 不重复处理。

  3. 扫描定性(Scan — White / Black):再次深度优先遍历。若 zval 的 refcount 已被减至 0,说明仅被循环引用维持,标记为白色(确认垃圾);若 refcount 仍 > 0,说明还有外部引用,恢复其 refcount 并标记为黑色(非垃圾)。

  4. 回收白色节点(Collect White):遍历根缓冲区,释放所有标记为白色的 zval,完成回收。

除了等缓冲区满自动触发外,也可通过 gc_collect_cycles() 随时手动触发收集。

 

结论:对开发的直接影响

  • PHP-FPM 短请求模式:请求结束后 SAPI 层释放整个请求内存,GC 压力极小,循环引用即使存在也不会长期累积。
  • 长驻进程(Swoole Worker、守护脚本、队列消费者):不存在"请求结束释放"的兜底,循环引用会持续累积,必须关注 GC 行为。
  • 批量处理场景:可通过 gc_disable() 暂时避免收集器频繁介入,处理完毕后 gc_enable() + gc_collect_cycles() 一次性清理。