memory_get_usage() 不降是因 zend mm 未还内存给 os,非 gc 失效;gc_collect_cycles() 返回 0 表示无待清理循环结构,不意味无垃圾;常驻进程中须主动调用以防止内存爬升。

unset 后 memory_get_usage() 不降,不是 GC 失效
这是最常被误读的现象:memory_get_usage() 返回的是 PHP 内存管理器(Zend MM)当前持有的堆内存字节数,不是操作系统 RSS。即使 zval 的 value 被释放,那块内存大概率仍留在 PHP 的空闲池里,等待后续复用——它根本没还给 OS。
验证方法:改用 memory_get_usage(true) 查看「实际向系统申请的内存页大小」,这个值在 GC 触发后才可能明显下降;尤其在 CLI 模式下反复测试时,必须加 true 参数才有参考意义。
- PHP 8.2 默认开启 GC,
unset($var)一定触发refcount__gc减 1,但仅当减到 0 且非循环引用时,value 才立即释放 - 字符串字面量(如
'hello')是 interned string,refcount恒为 0,unset完全不走 GC 流程 - 整型、布尔等标量在 PHP 7+ 中不参与引用计数,释放逻辑与 GC 无关
gc_collect_cycles() 返回 0,不代表没垃圾
gc_collect_cycles() 返回的是「本次扫描中成功清理的循环引用结构数量」,不是释放的内存字节数,也不是检测到的待回收对象个数。返回 0 只说明:当前根缓冲区(root buffer)里没有满足循环判定条件的节点,或者缓冲区压根没满、还没被填入候选对象。
PHP 的 GC 不是每次 unset 都扫描,它依赖「根缓冲区」积累:只有当变量被销毁但 refcount > 0(疑似循环),才会被暂存进缓冲区。默认阈值是 10000 个节点,没到就不自动触发。
- CLI 或 Swoole 常驻进程中,需手动调用
gc_collect_cycles()强制扫描,否则缓冲区可能长期不满,GC 永远不启动 - 调用后返回 0 是正常现象——说明当前没有可识别的循环结构,或已有结构已被提前打断(比如你先置
$obj->child = null) - 不要用返回值判断“是否该调用”,而应按业务节奏定期调用(例如每个请求处理完、每个任务循环末尾)
数组嵌套是循环引用高发区,但 gc_collect_cycles 不保证立刻见效
PHP 数组本身是 IS_TYPE_REFCOUNTED 类型,极易成为循环引用的载体。典型陷阱是:$a['ref'] = $a 或 $a['child'] = $b; $b['parent'] = $a。这类结构不会在 unset 时释放,必须靠 GC 的三色标记算法识别并清理。
但要注意:GC 扫描的是「根缓冲区中的候选节点」,不是全量变量。如果数组在函数内创建又销毁,其 zval 可能根本没进缓冲区——因为函数退出时局部变量 refcount 直接归零,走的是快速释放路径,不经过 GC。
- 常驻进程里,要避免在长生命周期数组中动态写入对象引用,尤其是 self-reference
- 用
gc_status()查看当前缓冲区使用量:gc_status()['roots'],确认是否有积压 - 若发现
roots持续增长但gc_collect_cycles()返回始终为 0,大概率是缓冲区未满 + 无显式调用,不是机制失效
PHP 8.2 的 GC 行为和旧版基本一致,但更依赖主动干预
PHP 8.2 没有重构 GC 底层逻辑,仍是「引用计数 + 根缓冲区 + 三色标记」的老架构。变化在于:Zend MM 内存分配器更激进地复用空闲页,导致 memory_get_usage() 更难回落;同时 JIT 和 OPcache 的介入让部分 zval 生命周期更隐蔽。
这意味着:你在 FPM 环境里几乎不用管 GC(请求结束整个内存空间就扔了),但在 Swoole/Workerman/CLI 长任务中,gc_collect_cycles() 不是“可选优化”,而是防止内存缓慢爬升的必要操作。
- 别等 OOM 才想起来调用——在每次大数组处理、批量对象构建之后,插一句
gc_collect_cycles() - 禁用 GC(
gc_disable())只应在极短生命周期脚本中考虑,常驻进程禁用等于自埋雷 - 真正难排查的从来不是 GC 是否工作,而是哪些变量在持续往根缓冲区里塞节点却没人清
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











