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。
核心流程分四步:
-
收集疑似根(Purple):当 zval 的 refcount 减少但未降至 0 时,该 zval 被标记为"疑似垃圾根"放入根缓冲区(root buffer)。根缓冲区固定容量为 10,000 个根节点(源码中的
GC_THRESHOLD_DEFAULT常量,可重新编译修改)。 -
模拟删除(Mark Grey):根缓冲区满时,收集算法启动。从每个疑似根出发做深度优先遍历,对所引用的每个 zval 执行 refcount−1(模拟移除引用),并标记为灰色。同一 zval 不重复处理。
-
扫描定性(Scan — White / Black):再次深度优先遍历。若 zval 的 refcount 已被减至 0,说明仅被循环引用维持,标记为白色(确认垃圾);若 refcount 仍 > 0,说明还有外部引用,恢复其 refcount 并标记为黑色(非垃圾)。
-
回收白色节点(Collect White):遍历根缓冲区,释放所有标记为白色的 zval,完成回收。
除了等缓冲区满自动触发外,也可通过 gc_collect_cycles() 随时手动触发收集。
结论:对开发的直接影响
- PHP-FPM 短请求模式:请求结束后 SAPI 层释放整个请求内存,GC 压力极小,循环引用即使存在也不会长期累积。
- 长驻进程(Swoole Worker、守护脚本、队列消费者):不存在"请求结束释放"的兜底,循环引用会持续累积,必须关注 GC 行为。
- 批量处理场景:可通过
gc_disable()暂时避免收集器频繁介入,处理完毕后gc_enable()+gc_collect_cycles()一次性清理。