對于PHP這種需要同時處理多個請求的程式來說,申請和釋放記憶體的時候應該慎之又慎,一不小心便會釀成大錯。另一方面,除了要安全的申請和釋放記憶體外,還應該做到記憶體的最小化使用,因為它可能要處理每秒鐘數以千計的請求,為了提高系統整體的性能,每一次操作都應該隻使用最少的記憶體,對于不必要的相同資料的複制則應該能免則免。我們來看下面這段PHP代碼:
<?php
$a = 'Hello NowaMagic!';
$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 NowaMagic!", 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 NowaMagic!"這個字元串在記憶體中的位置。但是在第三行: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;
/* Duplicate any allocated structures within the zval* */
zval_copy_ctor(varcopy);
/* 從符号表中删除原來的變量
* This will decrease the refcount of varval in the process
*/
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來實作。見下圖【在引用時強制複制!】
同樣,下面的這段代碼同樣會在核心中産生歧義,是以需要強制複制!
//上圖對應的代碼
$a = 1;
$b = &$a;
$c = $a;