# 引用計數和寫時拷貝
## 參考
[引用計數基本知識](https://www.php.net/manual/zh/features.gc.refcounting-basics.php)
[PHP的垃圾回收機制](https://www.cnblogs.com/xuxubaobao/p/10840176.html)
[# PHP內核探索之變量---變量的容器-Zval](https://blog.csdn.net/ohmygirl/article/details/41542445)
## 1.引用計數(標量類型)
在上一章我們已經講解了PHP變量的結構,其中了解到ref\_count\_\_gc和is\_ref\_\_gc是PHP的GC機制所需的很重要的兩個字段,我們就寫幾個例子來看一下這兩個變量是如何變化的。
**1.1創建變量時,會創建一個zval**
~~~
$str = "test zval";
xdebug_debug_zval('str');
結果:str: (refcount=1, is_ref=0)='test zval'
~~~
當使用$str="test zval";來創建變量時,會在當前作用域的符號表中插入新的符號(str),由于該變量是一個普通的變量,因此會生成一個refcount=1且is\_ref=0的zval容器。也就是說,實際上是這樣的:

**1.2變量賦值給另外一個變量時,會增加zval的refcount值.**
~~~
$str = "test zval";
$str2 = $str;
xdebug\_debug\_zval('str');
xdebug\_debug\_zval('str2');
結果:
str: (refcount=2, is_ref=0)='test zval'
str2: (refcount=2, is_ref=0)='test zval'
~~~
同時我們看到,str和是str2這兩個symbol的zval結構是一樣的。這里其實是PHP所做的一個優化,由于str和str2都是普通變量,因而它們指向了同一個zval,而沒有為str2開辟單獨的zval。這么做,可以在一定程度上節省內存。這時的str,str2與zval的對應關系是這樣的:

**1.3使用unset時,對減少相應zval的refcount值**
~~~
$str = "test zval";
$str3 = $str2 = $str;
xdebug\_debug\_zval('str');
unset($str2,$str3)
xdebug\_debug\_zval('str');
結果:
str: (refcount=3, is_ref=0)='test zval'
str: (refcount=1, is_ref=0)='test zval'
~~~
由于unset($str2,$str3)會將str2和str3從符號表中刪除,因此,在unset之后,只有str指向該zval,如下圖所示:

現在如果執行unset($str),則由于zval的refcount會減少到0,該zval會從內存中清理。這當然是最理想的情況。
但是事情并不總是那么樂觀。
## 2.引用計數(復合類型)
當考慮像array和object這樣的復合類型時,事情就稍微有點復雜. 與標量(scalar)類型的值不同,array和object類型的變量把它們的成員或屬性存在自己的符號表中。這意味著下面的例子將生成三個zval變量容器。
**2.1創建數組類型**
與標量這些普通變量不同,數組和對象這類復合型的變量在生成zval時,會為每個item項生成一個zval容器。例如:
~~~
$ar=array(
'id'=> 38,
'name'=>'shine'
);
xdebug\_debug\_zval('str');
結果:
ar: (refcount=1, is_ref=0)=array (
'id' => (refcount=1, is_ref=0)=38,
'name' => (refcount=1, is_ref=0)='shine'
)
~~~

**2.2添加一個已經存在的元素到數組中**
可以看出,變量$arr生成的過程中,共生成了3個zval容器(紅色部分標注)。對于每個zval而言,refcount的增減規則與普通變量的相同。例如,我們在數組中添加另外一個元素,并把$arr\['name'\]的值賦給它:
~~~
$ar=array(
'id'=> 38,
'name'=>'shine',
);
$ar['test']=$ar[’name‘];
xdebug_debug_zval('str');
結果:
arr: (refcount=1, is_ref=0)=array (
'id' => (refcount=1, is_ref=0)=38,
'name' => (refcount=1, is_ref=0)='shine',
'test' => (refcount=1, is_ref=0)='shine',
)
~~~
如同普通變量一樣,這時候,name和test這兩個symbol指向同一個zval:

**2.2unset刪除一個已經存在的元素到數組中**
同樣的,從數組中移除元素時,會從符號表中刪除相應的符號,同時減少對應zval的refcount值。同樣,如果zval的refcount值減少到0,那么就會從內存中刪除該zval:
~~~
$ar=array(
'id'=> 38,
'name'=>'shine',
);
$ar['test']=$ar[’name‘];
unset($ar['test'],$ar['name']);
xdebug_debug_zval('str');
結果:
ar: (refcount=1, is_ref=0)=array ('id' => (refcount=1, is_ref=0)=38)
~~~

現在,當我們添加一個數組本身作為這個數組的元素時,事情就變得有趣,下個例子將說明這個。例中我們加入了引用操作符,否則php將生成一個復制。
**2.3把數組作為一個元素添加到自己**
~~~
$a?\=?array(?'one'?);
$a\[\]?=&?$a;
xdebug\_debug\_zval(?'a'?);
結果:
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
~~~
能看到數組變量 (a) 同時也是這個數組的第二個元素(1) 指向的變量容器中“refcount”為`2`。上面的輸出結果中的"..."說明發生了遞歸操作, 顯然在這種情況下意味著"..."指向原始數組。

**2.4unset $a**
現在,我們對$a執行unset操作,這會在symbol table中刪除相應的symbol,同時,zval的refcount減1(之前為2),也就是說,現在的zval應該是這樣的結構:
~~~
(refcount=1, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=1, is_ref=1)=...
)
~~~

這時,不幸的事情發生了!
Unset之后,雖然沒有變量指向該zval,但是該zval卻不能被GC(指PHP5.3之前的單純引用計數機制的GC)清理掉,因為zval的refcount均大于0。這樣,這些zval實際上會一直存在內存中,直到請求結束(參考SAPI的生命周期)。在此之前,這些zval占據的內存不能被使用,便白白浪費了,換句話說,無法釋放的內存導致了內存泄露。
如果這種內存泄露僅僅發生了一次或者少數幾次,倒也還好,但如果是成千上萬次的內存泄露,便是很大的問題了。尤其在長時間運行的腳本中(例如守護程序,一直在后臺執行不會中斷),由于無法回收內存,最終會導致系統“再無內存可用”。
## 2.寫時copy
前面我們已經介紹過,在變量賦值的過程中例如$b = $a,為了節省空間,并不會為$a和$b都開辟單獨的zval,而是使用共享zval的形式:

**
**2.1如果其中一個變量發生變化時,如何處理zval的共享問題?**
~~~
$a = "a simple test";
$b = $a;
echo "before write:".PHP_EOL;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
$b = "thss"cho "after write:".PHP_EOL;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
結果:
before write:
a: (refcount=2, is_ref=0)='a simple test'
b: (refcount=2, is_ref=0)='a simple test'
after write:
a: (refcount=1, is_ref=0)='a simple test'
b: (refcount=1, is_ref=0)='thss
~~~
起初,符號表中a和b指向了同一個zval(這么做的原因是節省內存),而后$b發生了變化,Zend會檢查b指向的zval的refcount是否為1,如果是1,那么說明只有一個符號指向該zval,則直接更改zval。否則,說明這是一個共享的zval,需要將該zval分離出去,以保證單獨變化互不影響,這種機制叫做**COW**–Copy on write。在很多場景下,COW都是一種比較高效的策略。
**2.1那么對于引用變量呢?**
~~~
$a = 'test';
$b = &$a;
echo "before change:".PHP\_EOL;
xdebug\_debug\_zval('a');
xdebug\_debug\_zval('b');
$b = 12;
echo "after change:".PHP\_EOL;
xdebug\_debug\_zval('a');
xdebug\_debug\_zval('b');
unset($b);
echo "after unset:".PHP\_EOL;
xdebug\_debug\_zval('a');
xdebug\_debug\_zval('b');
結果:
before change:
a: (refcount=2, is_ref=1)='test'
b: (refcount=2, is_ref=1)='test'
after change:
a: (refcount=2, is_ref=1)=12
b: (refcount=2, is_ref=1)=12
after unset:
a: (refcount=1, is_ref=0)=12
~~~
可以看出,在改變了$b的值之后,Zend會檢查zval的is\_ref檢查是否是引用變量,如果是引用變量,則直接更改即可,否則,需要執行剛剛提到的zval分離。由于$a 和 $b是引用變量,因而更改共享的zval實際上也間接更改了$a的值。而在unset($b)之后,變量$b從符號表中刪除了。
這里也說明一個問題,unset并不是清除zval,而只是從符號表中刪除相應的symbol。
這一章我們講述了變量引用計數的相關原理,在下一章我們會對PHP的GC機制做一個總結