PHP5 中的 zval

typedef struct _zval_struct {
zvalue_value value;
zend_uint refcount__gc;
zend_uchar type;
zend_uchar is_ref__gc;
} zval;

如上,zval 包含一个 ​​value​​​、一个 ​​type​​​ 以及两个 ​​__gc​​​ 后缀的字段。​​value​​ 是个联合体,用于存储不同类型的值:

typedef union _zvalue_value {
long lval; // 用于 bool 类型、整型和资源类型
double dval; // 用于浮点类型
struct { // 用于字符串
char *val;
int len;
} str;
HashTable *ht; // 用于数组
zend_object_value obj; // 用于对象
zend_ast *ast; // 用于常量表达式(PHP5.6 才有)
} zvalue_value;

C 语言联合体的特征是一次只有一个成员是有效的并且分配的内存与需要内存最多的成员匹配(也要考虑内存对齐)。所有成员都存储在内存的同一个位置,根据需要存储不同的值。当你需要 ​​lval​​​ 的时候,它存储的是有符号整形,需要 ​​dval​​ 时,会存储双精度浮点数。

PHP5 中的引用计数

在PHP5中,zval 的内存是单独从堆(heap)中分配的(有少数例外情况),PHP 需要知道哪些 zval 是正在使用的,哪些是需要释放的。所以这就需要用到引用计数:zval 中 ​​refcount__gc​​​ 的值用于保存 zval 本身被引用的次数,比如 ​​$a = $b = 42​​​ 语句中,​​42​​ 被两个变量引用,所以它的引用计数就是 2。如果引用计数变成 0,就意味着这个变量已经没有用了,内存也就可以释放了。

注意这里提及到的引用计数指的不是 PHP 代码中的引用(使用 ​​&​​),而是变量的使用次数。后面两者需要同时出现时会使用『PHP 引用』和『引用』来区分两个概念,这里先忽略掉 PHP 的部分。

一个和引用计数紧密相关的概念是『写时复制』:对于多个引用来说,zaval 只有在没有变化的情况下才是共享的,一旦其中一个引用改变 zval 的值,就需要复制(”separated”)一份 zval,然后修改复制后的 zval。

下面是一个关于『写时复制』和 zval 的销毁的例子

<?php
$a = 42; // $a -> zval_1(type=IS_LONG, value=42, refcount=1)
$b = $a; // $a, $b -> zval_1(type=IS_LONG, value=42, refcount=2)
$c = $b; // $a, $b, $c -> zval_1(type=IS_LONG, value=42, refcount=3)
// 下面几行是关于 zval 分离的
$a += 1; // $b, $c -> zval_1(type=IS_LONG, value=42, refcount=2)
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)
unset($b); // $c -> zval_1(type=IS_LONG, value=42, refcount=1)
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)
unset($c); // zval_1 is destroyed, because refcount=0
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)