# 3.2 引用計數
# 3.2 引用計數
對于PHP這種需要同時處理多個請求的程序來說,申請和釋放內存的時候應該慎之又慎,一不小心便會釀成大錯。另一方面,除了要安全的申請和釋放內存外,還應該做到內存的最小化使用,因為它可能要處理每秒鐘數以千計的請求,為了提高系統整體的性能,每一次操作都應該只使用最少的內存,對于不必要的相同數據的復制則應該能免則免。我們來看下面這段PHP代碼:
```
<?php
$a = 'Hello World';
$b = $a;
unset($a);
```
第一條語句執行后,PHP創建了$a這個變量,并為它申請了12B的內存來存放"hello world"這個字符串(最后加個NULL字符,你懂的)。緊接著把$a賦給了$b,并釋放掉$a; 對于PHP來說,如果每一次變量賦值都執行一次內存復制的話,那需要額外申請12B的內存來存放這個重復的數據,當然為了復制內存,還需要cpu執行某些計算,這當然會加重cpu的負載。當第三句執行后,$a被釋放了,我們剛才的設想突然變的這么滑稽,這次賦值顯得好多余哦。如果早就知道$a不用了,那我們直接讓$b用$a的內存不就行了,還賦值干嘛?如果你覺得12B沒什么,那設想下如果$a是個10M的文件內容,或者20M,是不是我們的計算機資源消耗的有點冤枉呢? 別擔心,PHP很聰明! 前面章節說過,PHP變量的名稱和值在內核中是保存在兩個不同的地方的,值是通過一個與名字毫無關系的zval結構來保存,而這個變量的名字a則保存在符號表里,兩者之間通過指針聯系著。在我們上面的例子里,$a是一個字符串,我們通過zend\_hash\_add把它添加到符號表里,然后又把它賦值給$b,兩者擁有相同的內容!如果兩者指向完全相同的內容,我們有什么優化措施嗎?
```
zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);
//通過這個例子我們看出了,我們可以把$a和$b都指向helloval~!
```
現在我們檢查$a和$b兩個變量,他們的值指向了"hello world"這個字符串在內存中的位置。但是在第三行:unset($a);這條語句釋放了$a。在這種情況下,unset函數并不知道$a的值同時被$b用著,所以如果它直接釋放內存,則會導致$b的值也被清空了,從而導致邏輯錯誤,甚至可能會導致系統崩潰。 呵呵,其實你心里明白,PHP不會讓上述問題發生的!回顧一下zval的四個成員value、type、is\_ref**gc、refcount**gc,我們對value和type已經很熟了,現在則是后兩個成員發揮威力的時候了,這里我們主要講解refcount**gc這個成員。當一個變量被第一次創建的時候,它對應的zval結構體的refcount**gc成員的值會被初始化為1,理由很簡單,因為只有這個變量自己在用它。但是當你把這個變量賦值給別的變量時,refcount\_\_gc屬性便會加1變成2,因為現在有兩個變量在用這個zval結構了! 以上描述轉為內核中的代碼大體如下:
```
zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
ZVAL_ADDREF(helloval); //這句很特殊,我們顯式的增加了helloval結構體的refcount
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);
```
這個時候當我們再用unset刪除$a的時候,它刪除符號表里的$a的信息,然后清理它的值部分,這時它發現$a的值對應的zval結構的refcount值是2,也就是有另外一個變量在一起用著這個zval,所以unset只需把這個zval的refcount減去1就行了!
### 寫時復制機制
引用計數絕對是節省內存的一個超棒的模式!但是當我們修改$b的值,而且還需要繼續使用$a時,該怎么辦呢?
```
$a = 1;
$b = $a;
$b += 5;
```
從代碼邏輯來看,我們希望語句執行后$a仍然是1,而$b則需要變成6。我們知道在第二句完成后內核通過讓$a和$b共享一個zval結構來達到節省內存的目的,但是現在第三句來了,這時$b的改變應該怎樣在內核中實現呢? 答案非常簡單,內核首先查看refcount\_\_gc屬性,如果它大于1則為這個變化的變量從原zval結構中復制出一份新的專屬與$b的zval來,并改變其值。
```
zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
{
zval **varval, *varcopy;
if (zend_hash_find(EG(active_symbol_table),varname, varname_len + 1, (void**)&varval) == FAILURE)
{
/* 如果在符號表里找不到這個變量則直接return */
return NULL;
}
if ((*varval)->refcount < 2)
{
//如果這個變量的zval部分的refcount小于2,代表沒有別的變量在用,return
return *varval;
}
/* 否則,復制一份zval*的值 */
MAKE_STD_ZVAL(varcopy);
varcopy = *varval;
/* 復制任何在zval*內已分配的結構*/
zval_copy_ctor(varcopy);
/* 從符號表中刪除原來的變量
* 這將減少該過程中varval的refcount的值
*/
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
/* 初始化新的zval的refcount,并在符號表中重新添加此變量信息,并將其值與我們的新zval相關聯。*/
varcopy->refcount = 1;
varcopy->is_ref = 0;
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL);
/* 返回新zval的地址 */
return varcopy;
}
```
現在$b變量擁有了自己的zval,并且可以自由的修改它的值了。
### Change on Write
如果用戶在PHP腳本中顯式的讓一個變量引用另一個變量時,我們的內核是如何處理的呢?
```
$a = 1;
$b = &$a;
$b += 5;
```
作為一個標準的PHP程序猿,我們都知道$a的值也變成6了。當我們更改$b的值時,內核發現$b是$a的一個用戶端引用,也就是所它可以直接改變$b對應的zval的值,而無需再為它生成一個新的不同與$a的zval。因為他知道$a和$b都想得到這次變化! 但是內核是怎么知道這一切的呢?簡單的講,它是通過zval的is\_ref**gc成員來獲取這些信息的。這個成員只有兩個值,就像開關的開與關一樣。它的這兩個狀態代表著它是否是一個用戶在PHP語言中定義的引用。在第一條語句($a = 1;)執行完畢后,$a對應的zval的refcount**gc等于1,is\_ref**gc等于0;。 當第二條語句執行后($b = &$a;),refcount**gc屬性向往常一樣增長為2,而且is\_ref\_\_gc屬性也同時變為了1! 最后,在執行第三條語句的時候,內核再次檢查$b的zval以確定是否需要復制出一份新的zval結構來,這次不需要復制,因為我們剛才上面的get\_var\_and\_separate函數其實是個簡化版,并且少寫了一個條件:
```
/* 如果這個zval在php語言中是通過引用的形式存在的,或者它的refcount小于2,則不需要復制。*/
if ((*varval)->is_ref || (*varval)->refcount < 2) {
return *varval;
}
```
這一次,盡管它的refcount等于2,但是因為它的is\_ref等于1,所以也不會被復制。內核會直接的修改這個zval的值。
### Separation Anxiety
我們已經了解了php語言中變量的復制和引用的一些事,但是如果復制和引用這兩個事件被組合起來使用了該怎么辦呢?看下面這段代碼:
```
$a = 1;
$b = $a;
$c = &$a;
```
這里我們可以看到,$a,$b,$c這三個變量現在共用一個zval結構,有兩個屬于change-on-write組合($a,$c),有兩個屬于copy-on-write組合($a,$b),我們的is\_ref**gc和refcount**gc該怎樣工作,才能正確的處理好這段復雜的關系呢? The answer is: 不可能!在這種情況下,變量的值必須分離成兩份完全獨立的存在!$a與$c共用一個zval,$b自己用一個zval,盡管他們擁有同樣的值,但是必須至少通過兩個zval來實現。見圖3.2【在引用時強制復制!】

同樣,下面的這段代碼同樣會在內核中產生歧義,所以需要強制復制!
```
//上圖對應的代碼
$a = 1;
$b = &$a;
$c = $a;
```
圖3.3:

需要注意的是,在這兩種情況下,$b都與原初的zval相關聯,因為當復制發生時,內核還不知道第三個變量的名字。## links
- 3.1 [內存管理](3.1.html)
- 3.3 [3.3 第三章總結](3.3.html)
- 介紹
- 1 PHP的生命周期
- 1.1 讓我們從SAPI開始
- 1.2 PHP的啟動與終止
- 1.3 PHP的生命周期
- 1.4 線程安全
- 1.5 PHP的生命周期
- 2 PHP變量在內核中的實現
- 2.1 變量的類型
- 2.2 變量的值
- 2.3 創建PHP變量
- 2.4 變量的存儲方式
- 2.5 變量的檢索
- 2.6 類型轉換
- 2.7 小結
- 3 內存管理
- 3.1 內存管理
- 3.2 引用計數
- 3.3 內存管理
- 4 動手編譯PHP
- 4.1 動手編譯PHP
- 4.2 動手編譯PHP
- 4.3 Unix/Linux平臺下的編譯
- 4.4 在Win32平臺上編譯PHP
- 4.5 動手編譯PHP
- 5 Your First Extension
- 5.1 Your First Extension
- 5.2 編譯我們的擴展
- 5.3 靜態編譯
- 5.4 編寫函數
- 5.5 Your First Extension
- 6 函數返回值
- 6.1 函數返回值
- 6.2 引用與函數的執行結果
- 6.3 函數返回值
- 7 函數的參數
- 7.1 函數的參數
- 7.2 函數的參數
- 7.3 函數的參數
- 8 使用HashTable與{數組}
- 8.1 使用HashTable與{數組}
- 8.2 使用HashTable與{數組}
- 8.3 使用HashTable與{數組}
- 8.4 使用HashTable與{數組}
- 9 PHP中的資源類型
- 9.1 PHP中的資源類型
- 9.2 PHP中的資源類型
- 9.3 PHP中的資源類型
- 9.4 PHP中的資源類型
- 10 PHP中的面向對象(一)
- 10.1 PHP中的面向對象(一)
- 10.2 PHP中的面向對象(一)
- 10.3 PHP中的面向對象(一)
- 10.4 PHP中的面向對象(一)
- 10.5 PHP中的面向對象(一)
- 11 PHP中的面向對象(二)
- 11.1 PHP中的面向對象(二)
- 11.2 PHP中的面向對象(二)
- 11.3 PHP中的面向對象(二)
- 12 啟動與終止的那點事
- 12.1 關于生命周期
- 12.2 MINFO與phpinfo
- 12.3 常量
- 12.4 PHP擴展中的全局變量
- 12.5 PHP語言中的超級全局變量(Superglobals)
- 12.6 小結
- 13 INI設置
- 13.1 聲明和訪問INI設置
- 13.2 小結
- 14 流式訪問
- 14.1 流的概覽
- 14.2 訪問流
- 14.3 靜態資源操作
- 14.4 links
- 15 流的實現
- 15.1 php流的表象之下
- 15.2 包裝器操作
- 15.3 實現一個包裝器
- 15.4 操縱
- 15.5 檢查
- 15.6 小結
- 16 有趣的流
- 16.1 上下文
- 16.2 過濾器
- 16.3 小結
- 17 配置和鏈接
- 17.1 autoconf
- 17.2 庫的查找
- 17.3 強制模塊依賴
- 17.4 Windows方言
- 17.5 小結
- 18 擴展生成
- 18.1 ext_skel
- 18.2 PECL_Gen
- 18.3 小結
- 19 設置宿主環境
- 19.1 嵌入式SAPI
- 19.2 構建并編譯一個宿主應用
- 19.3 通過嵌入包裝重新創建cli
- 19.4 老技術新用
- 19.5 小結
- 20 高級嵌入式
- 20.1 回調到php中
- 20.2 錯誤處理
- 20.3 初始化php
- 20.4 覆寫INI_SYSTEM和INI_PERDIR選項
- 20.5 捕獲輸出
- 20.6 同時擴展和嵌入
- 20.7 小結