PHP 垃圾回收机制(GC)
一句话
PHP GC = 引用计数为主 + 循环引用收集器为辅。 引用计数归零立刻释放,解决不了的循环引用由后台收集器周期清理。
一、引用计数(refcount)
PHP 的每个变量底层是 zval,里面有 refcount 和 is_ref 两个字段。
1 | $a = 'hello'; // refcount = 1 |
用 xdebug_debug_zval() 或 debug_zval_refcount() 可以看到 refcount。
注意:PHP 7+ 的 zval 实现有重大改动(zval 嵌入栈、引用单独 zend_reference),但算法语义没变。
二、引用计数解决不了的:循环引用
1 | class Node { |
PHP 5.3 引入 循环引用收集器(Cycle Collector),定期扫描这些”孤岛”。
三、循环收集算法(同步 GC)
整套算法基于 IBM 的论文 “Concurrent Cycle Collection in Reference Counted Systems”:
- Roots Buffer:每次 refcount 减少但 ≠ 0 的 zval,被加入”可疑列表”
- 列表满了(默认 10000 个)触发:
- Mark:从可疑根出发,遍历能到达的所有 zval,refcount 全部 -1
- Scan:再遍历一次,refcount > 0 的还原(说明是外部引用)
- Collect:refcount = 0 的真正释放
1 | ; php.ini |
四、手动控制
1 | gc_enabled(); // GC 是否开启 |
何时手动调?
- CLI 长任务(队列消费者、daemon):每 N 个任务调一次,避免缓冲区慢慢撑爆
- 内存敏感的循环:处理大数组后立刻
unset()+gc_collect_cycles() - Swoole / Workerman:常驻进程必须关注 GC
五、内存泄漏排查思路
1 | // 1. 看当前内存 |
典型泄漏源:
| 场景 | 现象 | 修复 |
|---|---|---|
| 单例里挂 listener,listener 反向引用单例 | 长跑后 OOM | 用 WeakMap(PHP 8+)或显式 unset |
| 全局数组缓存无上限 | 内存稳步上涨 | LRU + 限容 |
| ORM 里 entity 互相 hasMany | 批量处理后没释放 | 处理完一批 detach |
static 局部变量累积 | 每次调用都涨 | 改成实例属性或外部缓存 |
六、PHP 8 新特性:WeakMap
1 | $cache = new WeakMap(); |
专门为缓存/装饰器场景设计,不影响目标对象的引用计数。
参考
- PHP 手册 - GC: https://www.php.net/manual/zh/features.gc.php
- Concurrent Cycle Collection in Reference Counted Systems: https://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon01Concurrent.pdf
https://mikeah2011.github.io/post/05_PHP/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.html
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Michael's Blog!
评论




