## 引用計數
迄今為止,我們向`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 指針的復雜類型(比如字符串、數組和對象)會自己存儲引用計數。這樣就可以有更少的內存分配操作、更少的間接指針使用以及更少的內存分配。