php 7 zval压缩至16字节的核心原因是结构重排与union复用:拆分value/u1/u2字段、小值内联、移除冗余对齐、引用计数下沉至具体数据结构,使整数/布尔等不再浪费空间。

zval 从 24 字节压缩到 16 字节,靠的是结构重排和 union 复用
PHP 5 的 zval 在 64 位系统下实际占 24 字节,主要因为 zvalue_value 联合体中最大成员(如 zend_object_value)拉高了对齐要求,加上独立的 refcount__gc、is_ref__gc 字段,冗余明显。PHP 7 把这堆字段“压扁”进两个 8 字节块:value(8 字节联合体) + u1+u2(共 8 字节),总长严格控制在 16 字节。
关键动作包括:
-
refcount不再存于zval本身,改由具体数据结构(如zend_string、zend_array)自行管理,zval只存指针或原生值 -
type和type_flags合并进u1.v.type,用单字节区分IS_LONG、IS_STRING等 10+ 类型,不再需要额外字段 -
value联合体里去掉 PHP 5 中冗余的str.len等内嵌字段,字符串长度交给zend_string结构自己管
小类型直接存值,大类型只存指针,避免无谓内存分配
PHP 7 的 zend_value 联合体决定了“什么该放进去,什么该甩出去”:整数、布尔、浮点这些小数据直接塞进 8 字节空间;而字符串、数组、对象这类复杂结构,zval 里只存一个指针(如 zend_string *),真实数据另起内存块。
这意味着:
-
$a = 42→zval.value.lval直接存 42,零额外分配 -
$b = "hello"→zval.value.str指向一块独立的zend_string内存,含len、h(hash 缓存)、val[] -
unset($b)后,zval类型变成IS_UNDEF,但背后的zend_string不立即释放——等 refcount 降为 0 才真回收
栈上预分配 zval,减少高频 malloc/free 开销
PHP 5 里每次创建变量都要调 MAKE_STD_ZVAL() 从堆上 malloc 一块内存;PHP 7 改成在函数栈帧里直接声明 zval val,或批量预分配一组 zval 池(如 VM 执行时的 CV 变量表)。这省掉了大量系统调用和堆管理碎片。
注意几个实操细节:
- 扩展开发中写
zval val;是安全的,但若需长期持有(比如存在全局哈希表里),必须确保它指向的数据(如zend_string)有正确 refcount -
ZVAL_COPY()不是简单 memcpy,它会递增被拷贝对象的 refcount,并处理IS_REFERENCE等特殊标记 - 直接赋值
zval new_val = old_val;是浅拷贝,仅复制 16 字节结构,不碰背后数据——这是性能关键,也是引用逻辑的起点
IS_UNDEF 和 IS_INDIRECT 类型让运行时更轻量
PHP 7 新增 IS_UNDEF 表示“已 unset 但尚未清理的槽位”,IS_INDIRECT 用于间接引用(如全局符号表里的 CV 变量)。它们不携带实际数据,zval.value 字段完全闲置,却能避免内存重排或 bucket 删除开销。
典型场景:
- 数组
unset($arr['key'])后对应 bucket 的zval.u1.v.type设为IS_UNDEF,后续foreach自动跳过,count($arr)也不计入 - 函数参数绑定时,
IS_INDIRECTzval的value.zv指向真正变量,实现“变量的变量”语义,不用每次都查符号表 - 这两种类型的存在,使得 hashtable 的 rehash 可以延迟执行,降低突发性内存抖动
ZVAL_DEREF() 何时触发解引用、zval_ptr_dtor() 怎么判断是否该释放底层数据。这些边界行为不看源码很容易踩空。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











