迄今为止,咱们向HashTables
中加入的zval
要么是新建的,要么是刚拷贝的。它们都是独立的,只占用本身的资源且只存在于某个HashTable中。做为一个语言设计的概念,建立和拷贝变量的方法是“很好”的,可是习惯了C程序设计就会知道,经过避免拷贝大块的数据(除非绝对必须)来节约内存和CPU时间并很多见。考虑这段用户代码:php
<?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,引擎作相似下面的事情:php7
{ 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,而不用去进行内容拷贝。ui
当用户空间代码调用unset($a)
,引擎对该变量执行zval_ptr_dtor()
。在前面用到的zval_ptr_dtor()
中,你看不到的事实是,这个调用没有必要销毁该zval和它的内容。实际工做是减小refcount。若是,且仅仅是若是,引用计数变成了0,Zend引擎会销毁该zval。设计
有些简单数据类型不须要单独分配内存,也不须要计数;PHP7中zval的long和double类型是 不须要
引用计数的。指针
php7的zval结构从新定义了,都有一个一样的头(zend_refcounted)用来存储引用计数:code
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;
有两种方法引用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==一、refcount==2,同时新zval则为is_ref=0、refcount=1。如今来看另外一块内容相同的代码块,只是顺序稍有不一样:ci
<?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为一、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 指针的复杂类型(好比字符串、数组和对象)会本身存储引用计数。这样就能够有更少的内存分配操做、更少的间接指针使用以及更少的内存分配。