unset() 不等于内存释放,仅当 zval 为引用计数类型且无循环引用时 refcount 归零才立即释放;循环引用需 GC 或手动断链才能回收。

PHP 的 unset() 不等于内存释放,循环引用不破环、不触发 GC 周期,内存就卡在那儿不动——尤其在 Swoole、Workerman 等常驻进程中,这是最隐蔽的内存泄漏源头。
refcount 为 0 时到底会不会立刻释放内存?
会,但仅限两类情况:
• 该 zval 类型是 IS_TYPE_REFCOUNTED(比如 array、object、string)
• 且不存在任何循环引用路径(即 refcount 归零后,它不处于 GC 根缓冲区中)
整型、浮点等标量在 PHP 7+ 中 refcount__gc 恒为 0,根本不走 GC 流程;unset($var) 只是符号表解绑 + refcount 减 1,是否释放全看减完是不是 0,以及有没有被其他 zval 暗中拉着。
为什么 xdebug_debug_zval() 显示 refcount > 0 却找不到谁在引用?
典型循环引用已形成,但外部变量已被 unset 或函数退出,zval 本身仍通过内部指针互指。
常见场景包括:
• array 嵌套赋值:$a['child'] = $b; $b['parent'] = $a;
• 对象属性双向绑定:$objA->ref = $objB; $objB->ref = $objA;
• 使用 & 引用数组元素后又 unset 外层变量
此时调用 gc_status(),roots 字段通常非零,说明这些 zval 已进入根缓冲区,等待 GC 扫描。
什么时候该手动调用 gc_collect_cycles()?
不能等默认阈值(10000 节点)才触发,尤其在以下情况:
• 长生命周期脚本中批量构造/销毁复杂结构(如解析大 JSON 后重建对象树)
• Swoole Worker 进程处理完一个高内存请求后,准备接下一个请求前
• 单次请求中反复创建含闭包或对象引用的临时容器(如事件监听器集合)
注意:gc_collect_cycles() 是同步阻塞操作,返回本次回收的 zval 数量;频繁调用有开销,建议结合 gc_status() 的 runs 和 collected 字段做轻量监控,而非无条件每轮都调。
立即学习“PHP免费学习笔记(深入)”;
打断循环引用比等 GC 更快更确定
GC 是兜底手段,不是首选方案。真正可控的释放方式是提前清空引用链:
• 在 unset 前显式置空关键字段:$a['child'] = null; $b['parent'] = null;
• 对象销毁前调用清理方法:$objA->clearReferences();
• 避免在构造函数中直接建立双向引用,改用 setter 或延迟绑定
这类操作能让 refcount 真正归零,触发即时释放,绕过 GC 周期和三色标记开销。很多“内存没降”的问题,其实根本不需要动 gc_collect_cycles(),只差一行 = null。



















