zval的refcount字段决定内存是否释放,大于0时不回收,归零时立即释放;循环引用需gc介入,refcount不归零;可用xdebug_debug_zval或memory_get_usage验证。

zval结构体里的refcount字段决定内存是否释放
PHP所有变量都存放在zval结构体中,而refcount是其中最关键的整数字段——它记录当前值被多少个变量(或符号)引用。只要refcount大于 0,这块内存就不会被回收;一旦降到 0,Zend 引擎立刻释放对应内存。
这个过程不依赖定时器或后台线程,是同步、即时的。比如:
$a = "hello"; $b = $a; // refcount 变为 2,不复制字符串 unset($a); // refcount 减为 1,内存仍保留 unset($b); // refcount 变为 0,字符串内存立即释放
注意:refcount只统计“普通引用”,不包含 PHP 中用 & 创建的引用(那种会触发 is_ref = 1,走另一套逻辑)。
赋值和函数传参时refcount自动增减,但有例外
大多数情况下,变量赋值、数组元素写入、函数参数传递(非引用传递)都会让目标zval的refcount加 1;作用域结束或调用unset()则减 1。但以下情况不会增加refcount:
-
foreach遍历数组时,默认是“只读副本”,不增加原数组元素的refcount - 将变量作为
return值传出函数时,如果返回的是局部变量,引擎可能直接转移所有权(refcount不变,原zval被复用) - 对
string或array做只读操作(如strlen()、count())完全不碰refcount
真正触发复制的,是“写时复制(Copy-on-Write)”:只有当你修改一个被多处引用的值时,引擎才分配新zval并调整refcount。
循环引用会让refcount卡在非零值,必须靠GC介入
当两个对象互相持有对方(比如$obj1->parent = $obj2 且 $obj2->child = $obj1),它们的refcount永远不会归零——即使整个结构已脱离作用域。这种孤立但不可达的对象,引用计数机制完全无感。
这时就得靠 PHP 的垃圾回收器(GC):
- GC 默认启用(
zend.enable_gc = On),通过根缓冲区收集疑似循环节点 - 调用
gc_collect_cycles()可强制扫描并清理一次,返回回收的周期数 - GC 不是实时的:缓冲区满(默认 10,000 个节点)或请求结束前才会触发
别指望unset()能解决这类问题——它只减refcount,而循环结构里每个节点的refcount始终 ≥ 1。
用xdebug_debug_zval()或memory_get_usage()验证refcount行为
光看代码很难确认refcount实际值,尤其涉及数组嵌套或对象属性时。最直接的方式是:
xdebug_debug_zval('my_var');
输出类似 my_var: (refcount=2, is_ref=0)='value'。没有 Xdebug?可以用内存差值法:
- 执行
$big = str_repeat('x', 10 * 1024 * 1024);后调用memory_get_usage() - 再
$copy = $big;,再次调用——内存几乎不涨,说明没复制,refcount已加 - 接着
unset($big);,内存也不掉;直到unset($copy);才回落
这种验证方式比理论更可靠,尤其在调试内存泄漏时——很多“泄漏”其实只是refcount没归零,而不是 GC 失效。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











