# 3.2 引用計數
對于PHP這種需要同時處理多個請求的程序來說,申請和釋放內存的時候應該慎之又慎,一不小心便會釀成大錯。另一方面,除了要安全的申請和釋放內存外,還應該做到內存的最小化使用,因為它可能要處理每秒鐘數以千計的請求,為了提高系統整體的性能,每一次操作都應該只使用最少的內存,對于不必要的相同數據的復制則應該能免則免。我們來看下面這段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,兩者擁有相同的內容!如果兩者指向完全相同的內容,我們有什么優化措施嗎?
````c
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結構了!
以上描述轉為內核中的代碼大體如下:
````c
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時,該怎么辦呢?
````c
$a = 1;
$b = $a;
$b += 5;
````
從代碼邏輯來看,我們希望語句執行后$a仍然是1,而$b則需要變成6。我們知道在第二句完成后內核通過讓$a和$b共享一個zval結構來達到節省內存的目的,但是現在第三句來了,這時$b的改變應該怎樣在內核中實現呢?
答案非常簡單,內核首先查看refcount__gc屬性,如果它大于1則為這個變化的變量從原zval結構中復制出一份新的專屬與$b的zval來,并改變其值。
````c
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腳本中顯式的讓一個變量引用另一個變量時,我們的內核是如何處理的呢?
<pre>
$a = 1;
$b = &$a;
$b += 5;
</pre>
作為一個標準的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函數其實是個簡化版,并且少寫了一個條件:
````c
/* 如果這個zval在php語言中是通過引用的形式存在的,或者它的refcount小于2,則不需要復制。*/
if ((*varval)->is_ref || (*varval)->refcount < 2) {
return *varval;
}
````
這一次,盡管它的refcount等于2,但是因為它的is_ref等于1,所以也不會被復制。內核會直接的修改這個zval的值。
### Separation Anxiety
我們已經了解了php語言中變量的復制和引用的一些事,但是如果復制和引用這兩個事件被組合起來使用了該怎么辦呢?看下面這段代碼:
````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【在引用時強制復制!】
<p style="text-align:center"><img src="http://www.walu.cc/phpbook/image/03fig02.jpg" /></p>
同樣,下面的這段代碼同樣會在內核中產生歧義,所以需要強制復制!
````php
//上圖對應的代碼
$a = 1;
$b = &$a;
$c = $a;
````
圖3.3:
<p style="text-align:center"><img src="http://www.walu.cc/phpbook/image/03fig03.jpg" /></p>
需要注意的是,在這兩種情況下,$b都與原初的zval相關聯,因為當復制發生時,內核還不知道第三個變量的名字。
## links
* 3.1 [內存管理](<3.1.md>)
* 3.3 [3.3 第三章總結](<3.3.md>)
- about
- 開始閱讀
- 目錄
- 1 PHP的生命周期
- 1.讓我們從SAPI開始
- 2.PHP的啟動與終止
- 3.PHP的生命周期
- 4.線程安全
- 5.小結
- 2 PHP變量在內核中的實現
- 1. 變量的類型
- 2. 變量的值
- 3. 創建PHP變量
- 4. 變量的存儲方式
- 5. 變量的檢索
- 6. 類型轉換
- 7. 小結
- 3 內存管理
- 1. 內存管理
- 2. 引用計數
- 3. 總結
- 4 動手編譯PHP
- 1. 編譯前的準備
- 2. PHP編譯前的config配置
- 3. Unix/Linux平臺下的編譯
- 4. 在Win32平臺上編譯PHP
- 5. 小結
- 5 Your First Extension
- 1. 一個擴展的基本結構
- 2. 編譯我們的擴展
- 3. 靜態編譯
- 4. 編寫函數
- 5. 小結
- 6 函數返回值
- 1. 一個特殊的參數:return_value
- 2. 引用與函數的執行結果
- 3. 小結
- 7 函數的參數
- 1. zend_parse_parameters
- 2. Arg Info 與類型綁定
- 3. 小結
- 8 使用HashTable與{數組}
- 1. 數組(C中的)與鏈表
- 2. 操作HashTable的API
- 3. 在內核中操作PHP語言中數組
- 4. 小結
- 9 PHP中的資源類型
- 1. 復合類型的數據——{資源}
- 2. Persistent Resources
- 3. {資源}自有的引用計數
- 4. 小結
- 10 PHP中的面向對象(一)
- 1. zend_class_entry
- 2. 定義一個類
- 3. 定義一個接口
- 4. 類的繼承與接口的實現
- 5. 小結
- 11 PHP中的面向對象(二)
- 1. 生成對象的實例與調用方法
- 2. 讀寫對象的屬性
- 3. 小結
- 12 啟動與終止的那點事
- 2. 小結
- 1. 關于生命周期
- 2. MINFO與phpinfo
- 3. 常量
- 4. PHP擴展中的全局變量
- 5. PHP語言中的超級全局變量
- 6. 小結
- 13 INI設置
- 1. 聲明和訪問ini設置
- 2. 小結
- 2. 小結
- 14 流式訪問
- 1. 概覽
- 2. 打開流
- 3. 訪問流
- 4. 靜態資源操作
- 5. 小結
- 15 流的實現
- 1. php流的表象之下
- 2. 包裝器操作
- 3. 實現一個包裝器
- 4. 操縱
- 5. 檢查
- 6. 小結
- 16 有趣的流
- 1. 上下文
- 2. 過濾器
- 3. 小結
- 17 配置和鏈接
- 1. autoconf
- 2. 庫的查找
- 3. 強制模塊依賴
- 4. Windows方言
- 5. 小結
- 18 擴展生成
- 1. ext_skel
- 2. PECL_Gen
- 3. 小結
- 19 設置宿主環境
- 1. 嵌入式SAPI
- 2. 構建并編譯一個宿主應用
- 3. 通過嵌入包裝重新創建cli
- 4. 老技術新用
- 5. 小結
- 20 高級嵌入式
- 1. 回調到php中
- 2. 錯誤處理
- 3. 初始化php
- 4. 覆寫INI_SYSTEM和INI_PERDIR選項
- 5. 捕獲輸出
- 6. 同時擴展和嵌入
- 7. 小結
- 約定