一、内存管理:引用计数与写时复制(Copy-on-Write)
PHP 的内存管理机制在早期版本中并不依赖完整的垃圾回收系统,而是主要依靠两种核心技术来实现高效的资源管理:
- 引用计数(refcount)
- 写时复制(Copy-on-Write, COW)
从 PHP 5.3 开始,虽然引入了针对循环引用的周期性垃圾回收(GC for circular references),但其核心仍以引用计数为主,辅以周期检测作为补充。
所有复杂数据类型,如字符串、数组、对象和资源,在底层结构中均包含一个公共头部:
typedef struct _zend_refcounted_h {
uint32_t refcount; // 引用计数
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags,
uint16_t gc_info
)
};
uint32_t type_info;
};
} zend_refcounted_h;
zval
zend_refcounted_h
上述结构是 zend_array、zend_object、zend_string 等类型的共同前缀。这些结构体都以该引用计数头开始。
zend_string
zend_array
zend_object
都以
zend_refcounted_h
开头。
需要注意的是,zval 本身并不直接存储 refcount,而是通过指针指向那些带有引用计数信息的复合结构。
zval
实例解析:
$a = [1, 2]; // 创建 zend_array,初始 refcount = 1 $b = $a; // $b 的 zval 指向同一 zend_array,refcount 增至 2 $b[0] = 99; // 发现 refcount > 1 → 触发 COW:复制整个 zend_array,$b 指向新副本
关键理解:变量之间是否共享内存,取决于其所指向的底层结构(如 zend_array)的引用计数状态,而非 zval 自身的值或地址。
二、变量传递:值传递与引用传递的本质区别
在 PHP 中,默认采用“值传递”方式,但对于复杂类型(如数组、对象),实际行为更接近于“具有引用语义的值传递”。
1. 默认值传递下的写时复制机制
function foo($arr) {
$arr[0] = 99;
}
$data = [1, 2];
foo($data);
// $data 未被修改 → 因为函数内对数组的修改触发了 COW 机制(refcount 从 1 变为 2 后发生复制)
2. 显式引用传递:共享同一个容器
function bar(&$arr) {
$arr[0] = 99;
}
bar($data); // $data 被成功修改 → 因为形参 $arr 与实参 $data 共享同一个 zval 容器
底层差异分析:
- 值传递:复制的是
zval结构本身,但如果传递的是数组(IS_ARRAY 类型),其内部的value.arr依然指向原始的zend_array,直到发生修改时才触发 COW 分离。 - 引用传递:函数参数的
zval与调用者的变量使用相同的内存地址,这种绑定通过特殊的IS_REFERENCE类型实现。
IS_REFERENCE
注意:PHP 中的“引用”并非 C 语言中的指针概念,而是在 zval 层级上实现的别名机制 —— 多个变量名可绑定到同一个值容器。
zval
三、引用机制的演进:& 操作符与 IS_REFERENCE 类型
自 PHP 7 起,语言引入了专用的 IS_REFERENCE 类型来统一处理引用语义。
$a = 10;
$b = &$a; // 此时 $a 和 $b 都是 IS_REFERENCE 类型的 zval
// 它们共同指向一个 zend_reference 结构,其中封装了真实的值
对应的底层结构定义如下:
typedef struct _zend_reference {
zend_refcounted_h gc;
zval val; // 存储实际的数据值(例如 10)
// ...
} zend_reference;
这一设计的优势在于:
- 实现了真正的变量别名效果:修改其中一个,另一个同步更新;
- 解决了旧版 PHP 中因 zval 分离导致引用关系断裂的问题。
引用的本质变迁:不再是直接指向 zend_array 或其他结构的“指针”,而是通过一个中介结构(zend_reference)来统一管理多个 zval 对同一值的共享访问。
&
IS_REFERENCE
四、序列化原理:如何将运行时状态“冻结”为字符串?
PHP 序列化面临的核心挑战是如何将包含指针、引用关系、类元信息以及私有属性的复杂运行时结构,转化为可逆的纯文本表示形式。
以数组为例,序列化过程大致如下:
- 遍历
zend_array中的每一个元素(每个元素都是一个zval); - 对每个
zval进行类型判断并分别处理: - 若为标量类型:直接记录类型标识与数值(如
i:1;表示整数 1); - 若为嵌套数组或对象:递归执行序列化,并使用特定标记(如
a:2:{})包裹内容; - 若为对象:保存类名及所有可见属性(私有属性转换为
ClassNamepropName格式); - 对于重复出现的对象或数组(即存在引用),使用引用标记(如
r:2;)避免无限递归。
array
arData
zval
i:10;
IS_ARRAY
a:
IS_OBJECT
\0类名\0属性名
R:
反序列化过程则是逆向操作:
- 根据序列化字符串逐条解析指令;
- 重建完整的
zval层级结构; - 重新构造
zend_array和zend_object实例; - 恢复原有的引用关系(通过
r:类型的指针重建共享结构)。
重要提示:反序列化不会调用对象的构造函数(__construct),但会在完成后自动触发 __wakeup() 方法(如果已定义)。
__construct
__wakeup()
五、四大机制的统一基础:zval 模型
无论是内存管理、变量传递、引用机制还是序列化功能,它们在底层都围绕着 PHP 的核心数据单元 —— zval 构建而成。以下是各机制所依赖的关键结构与行为总结:
| 主题 | 依赖的底层结构 | 核心机制 |
|---|---|---|
| 内存管理 | zend_refcounted_h(集成于 zend_array、zend_object、zend_string 等) |
引用计数 + 写时复制(COW) |
| 变量传递 | zval 的复制策略 |
是否共享底层结构(由 refcount 控制) |
| 引用机制 | IS_REFERENCE 类型的 zval + zend_reference 结构 |
通过中间容器实现多变量别名绑定 |
zend_refcounted_h
zval
IS_REFERENCE别名绑定与序列化机制
在处理 PHP 变量时,遍历 zval 树并重建其结构是一个核心过程。这一过程依赖于类型标记、递归操作以及引用去重技术,确保数据结构的准确还原和内存使用的高效性。
zend_reference
六、PHP 变量内存模型图解
[PHP 变量 $x]
│
▼
+--------+ 若类型是 IS_ARRAY
| zval | ────────────────? +------------------+
| type | | zend_array |
| value | ?──────────────── | - refcount |
+--------+ (指针) | - arData[zval] |
+------------------+
│
▼ 若类型是 IS_OBJECT
+--------+ ────────────────? +------------------+
| zval | | zend_object |
| type | | - ce (类) |
| value | ?──────────────── | - properties |
+--------+ (指针) +------------------+
│
▼ 若是 &$y
+--------+ ────────────────? +------------------+
| zval | | zend_reference |
| type=IS_REFERENCE | - val (zval) |
| value | ?──────────────── +------------------+
+--------+
七、对开发者的实践建议
避免不必要的数组或对象复制
当传递大型数组作为函数参数时,应考虑使用引用传递或通过对象封装来减少内存开销。
readonly
谨慎使用序列化(serialize)
序列化操作会重建整个对象图,不仅性能消耗较大,还可能引发逻辑错误或安全漏洞,需特别注意使用场景。
unserialize()
理解赋值操作的潜在影响
在 PHP 中,数组赋值默认会发生复制行为。尽管 PHP 7+ 对此进行了优化,可能延迟复制(写时复制,COW),但一旦发生修改,仍会触发实际的数据拷贝。
foreach
调试内存引用状态
可以借助
xdebug_debug_zval()
或
refcount
等扩展工具,观察变量的引用计数与共享状态,帮助诊断内存问题。
最后:为何需要深入理解这套机制?
因为 PHP 表面看似简单的语法,背后实则运行着一套高度优化的底层模型。
你所编写的每一个
$arr = [1,2];
实际上都依托于
zval
→
zend_array
→
Bucket[zval]
这一三层结构,并结合引用计数与写时复制机制,才实现了动态数组“灵活且高效”的特性。
掌握这一内存模型,不仅能提升代码性能,更能在面对内存泄漏、异常引用行为或序列化问题时,迅速定位根本原因,直达问题本质。
若你有兴趣,我们还可进一步探讨:
- 分析
foreach
readonly
这些内容,正是从“会用 PHP”迈向“真正驾驭 PHP”的关键转折点。


雷达卡


京公网安备 11010802022788号







