🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 引用计数 迄今为止,我们向`HashTables`中加入的`zval`要么是新建的,要么是刚拷贝的。它们都是独立的,只占用自己的资源且只存在于某个HashTable中。作为一个语言设计的概念,创建和拷贝变量的方法是“很好”的,但是习惯了C程序设计就会知道,通过避免拷贝大块的数据(除非绝对必须)来节约内存和CPU时间并不少见。考虑这段用户代码: ``` <?php $a = file_get_contents('fourMegabyteLogFile.log'); $b = $a; unset($a); ``` 如果执行`zval_copy_ctor()`(将会对字符串内容执行`estrndup()`)将`$a`拷贝给`$b`,那么这个简短的脚本实际会用掉8M内存来存储同一4M文件的两份相同的副本。在最后一步取消$a只会更糟,因为原始字符串被`efree()`了。用C做这个将会很简单,大概是这样:`b = a; a = NULL;`。 Zend引擎的做法更聪明。当创建$a时,会创建一个潜在的string类型的zval,它含有日至文件的内容。这个`zval`通过调用`zend_hash_add()`被赋给`$a`变量。当`$a`被拷贝给`$b`,引擎做类似下面的事情: ```c { zval **value; zend_hash_find(EG(active_symbol_table), "a", sizeof("a"), (void**)&value); ZVAL_ADDREF(*value); zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), value,sizeof(zval*)); } ``` 实际代码会更复杂点,简单的说,就是通过引用计数来记录zval在符号表中、数组中、或其他地方被引用的次数。这样`$b = $a`赋值只要将其引用计数`+1`,而不用去进行内容拷贝。 当用户空间代码调用`unset($a)`,引擎对该变量执行`zval_ptr_dtor()`。在前面用到的`zval_ptr_dtor()`中,你看不到的事实是,这个调用没有必要销毁该`zval`和它的内容。实际工作是减少`refcount`。如果,且仅仅是如果,引用计数变成了0,Zend引擎会销毁该`zval`。 有些简单数据类型不需要单独分配内存,也不需要计数;PHP7中`zval`的`long`和`double`类型是 不需要 引用计数的。 php7的zval结构重新定义了,都有一个同样的头(`zend_refcounted`)用来存储引用计数: ```c typedef struct _zend_refcounted_h { uint32_t refcount; /* reference counter 32-bit */ union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, zend_uchar flags, /* used for strings & objects */ uint16_t gc_info) /* keeps GC root number (or 0) and color */ } v; uint32_t type_info; } u; } zend_refcounted_h; ``` ## 拷贝 vs 引用 有两种方法引用`zval`。第一种,如上文示范的,被称为写复制引用(copy-on-write referencing)。第二种形式是完全引用(full referencing);当说起“引用”时,用户空间代码的编写者更熟悉这种, 以用户空间代码的形式出现类似于:`$a = &$b;`。 在`zval`中,这两种类型的区别在于它的`is_ref`成员的值,`0`表示写复制引用,非`0`表示完全引用。注意,一个`zval`不可能同时具有两种引用类型。所以,如果变量起初是`is_ref`(即完全引用-译注),然后以拷贝的方式赋给新的变量,那么必将执行一个完全拷贝。考虑下面的用户空间代码: ``` <?php $a = []; //$a -> zend_array_1(refcount=1, value=[]) $b = &$a; //$a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[]) $c = $a; //// $a, $b, $c -> zend_array_1(refcount=3, value=[]) ``` 在这段代码中,为`$a`创建并初始化了一个`zval`,将`is_ref`设为`0`,将`refcount`设为`1`。当`$a`被`$b`引用时,`is_ref`变为1,`refcount`递增至`2`。当拷贝至`$c`时,Zend引擎不能只是递增`refcount`至`3`,因为如此则`$c`变成了`$a`的完全引用。关闭`is_ref`也不行,因为如此会使`$b`看起来像是`$a`的一份拷贝而不是引用。 所以此时分配了一个新的`zval`,并使用`zval_copy_ctor()`把原始(zval)的值拷贝给它。原始`zval`仍为`is_ref==1、refcount==2`,同时新`zval`则为`is_ref=0、refcount=1`。现在来看另一块内容相同的代码块,只是顺序稍有不同: ``` <?php $a = []; //$a -> zend_array_1(refcount=1, value=[]) $c = $a; // $a, $c -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) $b = &$a; // $c -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) // $b, $a -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[]) // $b 是 $a 的引用, 但却不是 $a 的 $c, 所以这里 zval 还是需要进行复制 // 这样我们就有了两个 zval, 一个 is_ref 的值是 0, 一个 is_ref 的值是 1. ``` 所有的变量都可以共享同一个数组,最终结果不变,`$b`是`$a`的完全引用,并且`$c`是`$a`的一份拷贝。然而这次的内部效果稍有区别。如前,开始时为`$a`创建一个`is_ref==0`并且`refcount=1`的新`zval`。`$c = $a;`语句将同一个`zval`赋给`$c`变量,同时将`refcount`增至`2`,`is_ref`仍是`0`。当Zend引擎遇到`$b = &$a;`,它想要只是将`is_ref`设为`1`,但是当然不行,因为那将影响到`$c`。所以改为创建新的zval并用`zval_copy_ctor()`将原始(zval)的内容拷贝给它。然后递减原始zval的`refcount`以表明`$a`不再使用该`zval`。代替地,(Zend)设置新zval的`is_ref`为`1`、`refcount`为`2`,并且更新`$a`和`$b`变量指向它(新zval)。 ``` <?php $a = []; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) $b = $a; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[]) // $b = zval_2(type=IS_ARRAY) ---^ // zval 分离在这里进行 $a[] = 1 // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1]) // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) unset($a); // $a = zval_1(type=IS_UNDEF), zend_array_2 被销毁 // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) ``` 这个过程其实挺简单的。现在整数不再是共享的,变量直接就会分离成两个单独的 `zval`,由于现在 `zval` 是内嵌的所以也不需要单独分配内存,所以这里的注释中使用 = 来表示的而不是指针符号 `->`,`unset` 时变量会被标记为 IS_UNDEF。 ## 总结 PHP7 中最重要的改变就是 zval 不再单独从堆上分配内存并且不自己存储引用计数。需要使用 zval 指针的复杂类型(比如字符串、数组和对象)会自己存储引用计数。这样就可以有更少的内存分配操作、更少的间接指针使用以及更少的内存分配。