PHP变量内存管理依赖zval结构和引用计数规则:zval含value、type、refcount、is_ref四字段;refcount控制堆内存增减与释放;写时复制(COW)实现高效共享;循环引用由GC兜底处理。

PHP变量的内存管理不靠玄学,靠的是zval结构和一套清晰的引用计数规则。理解它,才能避开隐性内存膨胀、意外延迟释放、循环引用泄漏等真实问题。
zval 是什么:变量在内核里的“身份证”
每个 PHP 变量背后都有一个 zval 结构体,它不是单纯存值,而是打包了四类关键信息:
-
value:联合体(union),按类型复用同一块内存——整数走
lval,字符串走str指针,数组走arr指针,对象走obj指针; -
type:标明当前是
IS_STRING、IS_ARRAY还是IS_OBJECT等; - refcount:记录有多少个变量名(symbol)正指向这个 zval 的数据;
-
is_ref:标识是否属于用户显式声明的引用(即用了
&$a),影响写时复制行为。
注意:像整数、布尔、NULL 这类小类型,值直接塞进 zval 里,不额外分配堆内存,也不参与引用计数;而字符串、数组、对象等大结构,zval 中只存指针,真正的数据在堆上,由 zend_refcounted 头部管理 refcount。
引用计数怎么工作:增、减、清零三步闭环
refcount 不是装饰,它直接决定内存何时回收:
立即学习“PHP免费学习笔记(深入)”;
- 赋值
$b = $a→ 若$a指向的是可计数类型(如字符串),则对应堆数据的 refcount +1; - 变量离开作用域(如函数返回)、或调用
unset($a)→ refcount -1; - refcount 降为 0 → Zend 引擎立刻释放该堆内存,不等 GC 触发;
- 若 refcount > 0,即使变量名消失,数据仍驻留,这是共享的基础,也是泄漏的温床。
用 xdebug_debug_zval('var') 可实时查看 refcount 和 is_ref 状态,调试内存异常时非常直观。
写时复制(COW):共享与隔离的平衡术
PHP 不在赋值时复制大数据,而是在**首次修改**时才分家:
-
$a = str_repeat('x', 1024*1024); $b = $a;→ 共享同一段 1MB 字符串内存,refcount=2; -
$b .= 'y';→ 检测到写操作,且 refcount > 1,于是复制出新字符串给$b,$a保持原样; - 但如果
$a已是引用(is_ref == 1),哪怕 refcount=2,修改$a也会同步影响$b,COW 被绕过。
COW 显著减少内存占用,但也意味着:看似安全的“只读共享”,一旦某处悄悄修改(比如函数内 push 数组),就触发复制,开销可能远超预期。
循环引用与 GC:refcount 的盲区补丁
引用计数无法处理 A→B→A 这类闭环。例如对象属性互相持有对方,即使外部所有变量都 unset,它们的 refcount 始终 ≥1,内存永不释放。
PHP 自 5.3 起引入周期性垃圾回收器(GC):
- 当根缓冲区(root buffer)满(默认 10,000 个 zval)或手动调用
gc_collect_cycles()时触发; - 采用深度优先标记算法,从全局符号表、函数栈等“根”出发,标记所有可达 zval;
- 未被标记但 refcount > 0 的 zval,判定为循环引用孤岛,强制清理。
GC 不高频运行,也不替代 refcount 主逻辑——它是兜底机制,不能依赖它来“救”设计不当的引用结构。



















