淺談PHP5中垃圾回收算法(Garbage Collection)的演化

文章來源:PHP開發學習門戶php

地址:http://www.phpthinking.com/archives/636程序員


前言

PHP是一門託管型語言,在PHP編程中程序員不須要手工處理內存資源的分配與釋放(使用C編寫PHP或Zend擴展除外),這就意味着PHP自己實現了垃圾回收機制(Garbage Collection)。如今若是去PHP官方網站能夠看到,目前PHP5的兩個分支版本PHP5.2和PHP5.3是分別更新的,這是由於許多項目仍然使用5.2版本的PHP,而5.3版本對5.2並非徹底兼容。PHP5.3在PHP5.2的基礎上作了諸多改進,其中垃圾回收算法就屬於一個比較大的改變。本文將分別討論PHP5.2和PHP5.3的垃圾回收機制,並討論這種演化和改進對於程序員編寫PHP的影響以及要注意的問題。算法

PHP變量及關聯內存對象的內部表示

垃圾回收說究竟是對變量及其所關聯內存對象的操做,因此在討論PHP的垃圾回收機制以前,先簡要介紹PHP中變量及其內存對象的內部表示(其C源代碼中的表示)。編程

PHP官方文檔中將PHP中的變量劃分爲兩類:標量類型和複雜類型。標量類型包括布爾型、整型、浮點型和字符串;複雜類型包括數組、對象和資源;還有一個NULL比較特殊,它不劃分爲任何類型,而是單獨成爲一類。數組

全部這些類型,在PHP內部統一用一個叫作zval的結構表示,在PHP源代碼中這個結構名稱爲「_zval_struct」。zval的具體定義在PHP源代碼的「Zend/zend.h」文件中,下面是相關代碼的摘錄。服務器


typedef  union  _zvalue_value {
     long  lval;                   /* long value */
     double  dval;                 /* double value */
     struct  {
         char  *val;
         int  len;
     } str;
     HashTable *ht;               /* hash table value */
     zend_object_value obj;
} zvalue_value;
struct  _zval_struct {
     /* Variable information */
     zvalue_value value;      /* value */
     zend_uint refcount__gc;
     zend_uchar type;     /* active type */
     zend_uchar is_ref__gc;
};

其中聯合體「_zvalue_value」用於表示PHP中全部變量的值,這裏之因此使用union,是由於一個zval在一個時刻只能表示一種類型的變量。能夠看到_zvalue_value中只有5個字段,可是PHP中算上NULL有8種數據類型,那麼PHP內部是如何用5個字段表示8種類型呢?這算是PHP設計比較巧妙的一個地方,它經過複用字段達到了減小字段的目的。例如,在PHP內部布爾型、整型及資源(只要存儲資源的標識符便可)都是經過lval字段存儲的;dval用於存儲浮點型;str存儲字符串;ht存儲數組(注意PHP中的數組實際上是哈希表);而obj存儲對象類型;若是全部字段所有置爲0或NULL則表示PHP中的NULL,這樣就達到了用5個字段存儲8種類型的值。ide

而當前zval中的value(value的類型便是_zvalue_value)到底表示那種類型,則由「_zval_struct」中的type肯定。_zval_struct便是zval在C語言中的具體實現,每一個zval表示一個變量的內存對象。除了value和type,能夠看到_zval_struct中還有兩個字段refcount__gc和is_ref__gc,從其後綴就能夠判定這兩個傢伙與垃圾回收有關。沒錯,PHP的垃圾回收全靠這倆字段了。其中refcount__gc表示當前有幾個變量引用此zval,而is_ref__gc表示當前zval是否被按引用引用,這話聽起來很拗口,這和PHP中zval的「Write-On-Copy」機制有關,因爲這個話題不是本文重點,所以這裏再也不詳述,讀者只需記住refcount__gc這個字段的做用便可。函數

PHP5.2中的垃圾回收算法——Reference Counting

PHP5.2中使用的內存回收算法是大名鼎鼎的Reference Counting,這個算法中文翻譯叫作「引用計數」,其思想很是直觀和簡潔:爲每一個內存對象分配一個計數器,當一個內存對象創建時計數器初始化爲1(所以此時老是有一個變量引用此對象),之後每有一個新變量引用此內存對象,則計數器加1,而每當減小一個引用此內存對象的變量則計數器減1,當垃圾回收機制運做的時候,將全部計數器爲0的內存對象銷燬並回收其佔用的內存。而PHP中內存對象就是zval,而計數器就是refcount__gc。性能

例以下面一段PHP代碼演示了PHP5.2計數器的工做原理(計數器值經過xdebug獲得):學習


<?php
$val1  = 100;  //zval(val1).refcount_gc = 1;
$val2  $val1 //zval(val1).refcount_gc = 2,zval(val2).refcount_gc = 2
//(由於是Write on copy,當前val2與val1共同引用一個zval)
$val2  = 200;  //zval(val1).refcount_gc = 1,zval(val2).refcount_gc = 1
//(此處val2新建了一個zval)
unset( $val1 );  //zval(val1).refcount_gc = 0
//($val1引用的zval不再可用,會被GC回收)
?>

Reference Counting簡單直觀,實現方便,但卻存在一個致命的缺陷,就是容易形成內存泄露。不少朋友可能已經意識到了,若是存在循環引用,那麼Reference Counting就可能致使內存泄露。例以下面的代碼:


<?php
$a  array ();
$a [] = &  $a ;
unset( $a );
?>

這段代碼首先創建了數組a,而後讓a的第一個元素按引用指向a,這時a的zval的refcount就變爲2,而後咱們銷燬變量a,此時a最初指向的zval的refcount爲1,可是咱們再也沒有辦法對其進行操做,由於其造成了一個循環自引用,以下圖所示:

image

其中灰色部分表示已經不復存在。因爲a以前指向的zval的refcount爲1(被其HashTable的第一個元素引用),這個zval就不會被GC銷燬,這部份內存就泄露了。

這裏特別要指出的是,PHP是經過符號表(Symbol Table)存儲變量符號的,全局有一個符號表,而每一個複雜類型如數組或對象有本身的符號表,所以上面代碼中,a和a[0]是兩個符號,可是a儲存在全局符號表中,而a[0]儲存在數組自己的符號表中,且這裏a和a[0]引用同一個zval(固然符號a後來被銷燬了)。但願讀者朋友注意分清符號(Symbol)的zval的關係。

在PHP只用於作動態頁面腳本時,這種泄露也許不是很要緊,由於動態頁面腳本的生命週期很短,PHP會保證當腳本執行完畢後,釋放其全部資源。可是PHP發展到目前已經不只僅用做動態頁面腳本這麼簡單,若是將PHP用在生命週期較長的場景中,例如自動化測試腳本或deamon進程,那麼通過屢次循環後積累下來的內存泄露可能就會很嚴重。這並非我在聳人聽聞,我曾經實習過的一個公司就經過PHP寫的deamon進程來與數據存儲服務器交互。

因爲Reference Counting的這個缺陷,PHP5.3改進了垃圾回收算法。

PHP5.3中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems

PHP5.3的垃圾回收算法仍然以引用計數爲基礎,可是再也不是使用簡單計數做爲回收準則,而是使用了一種同步回收算法,這個算法由IBM的工程師在論文Concurrent Cycle Collection in Reference Counted Systems中提出。

這個算法可謂至關複雜,從論文29頁的數量我想你們也能看出來,因此我不打算(也沒有能力)完整論述此算法,有興趣的朋友能夠閱讀上面的提到的論文(強烈推薦,這篇論文很是精彩)。

我在這裏,只能大致描述一下此算法的基本思想。

首先PHP會分配一個固定大小的「根緩衝區」,這個緩衝區用於存放固定數量的zval,這個數量默認是10,000,若是須要修改則須要修改源代碼Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES而後從新編譯。

由上文咱們能夠知道,一個zval若是有引用,要麼被全局符號表中的符號引用,要麼被其它表示複雜類型的zval中的符號引用。所以在zval中存在一些可能根(root)。這裏咱們暫且不討論PHP是如何發現這些可能根的,這是個很複雜的問題,總之PHP有辦法發現這些可能根zval並將它們投入根緩衝區。

當根緩衝區滿額時,PHP就會執行垃圾回收,此回收算法以下:

一、對每一個根緩衝區中的根zval按照深度優先遍歷算法遍歷全部能遍歷到的zval,並將每一個zval的refcount減1,同時爲了不對同一zval屢次減1(由於可能不一樣的根能遍歷到同一個zval),每次對某個zval減1後就對其標記爲「已減」。

二、再次對每一個緩衝區中的根zval深度優先遍歷,若是某個zval的refcount不爲0,則對其加1,不然保持其爲0。

三、清空根緩衝區中的全部根(注意是把這些zval從緩衝區中清除而不是銷燬它們),而後銷燬全部refcount爲0的zval,並收回其內存。

若是不能徹底理解也沒有關係,只需記住PHP5.3的垃圾回收算法有如下幾點特性:

一、並非每次refcount減小時都進入回收週期,只有根緩衝區滿額後在開始垃圾回收。

二、能夠解決循環引用問題。

三、能夠總將內存泄露保持在一個閾值如下。

PHP5.2與PHP5.3垃圾回收算法的性能比較

因爲我目前條件所限,我就不從新設計試驗了,而是直接引用PHP Manual中的實驗,關於二者的性能比較請參考PHP Manual中的相關章節:http://www.php.net/manual/en/features.gc.performance-considerations.php

首先是內存泄露試驗,下面直接引用PHP Manual中的實驗代碼和試驗結果圖:


<?php
class  Foo
{
     public  $var  '3.1415962654' ;
}
$baseMemory  = memory_get_usage();
for  $i  = 0;  $i  <= 100000;  $i ++ )
{
     $a  new  Foo;
     $a ->self =  $a ;
     if  $i  % 500 === 0 )
     {
         echo  sprintf(  '%8d: ' $i  ), memory_get_usage() -  $baseMemory "\n" ;
     }
}
?>

PHP內存泄露試驗

能夠看到在可能引起累積性內存泄露的場景下,PHP5.2發生持續累積性內存泄露,而PHP5.3則總能將內存泄露控制在一個閾值如下(與根緩衝區大小有關)。

另外是關於性能方面的對比:


<?php
class  Foo
{
     public  $var  '3.1415962654' ;
}
for  $i  = 0;  $i  <= 1000000;  $i ++ )
{
     $a  new  Foo;
     $a ->self =  $a ;
}
echo  memory_get_peak_usage(),  "\n" ;
?>

這個腳本執行1000000次循環,使得延遲時間足夠進行對比。

而後使用CLI方式分別在打開內存回收和關閉內存回收的的狀況下運行此腳本:


time  php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
# and
time  php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php

在個人機器環境下,運行時間分別爲6.4s和7.2s,能夠看到PHP5.3的垃圾回收機制會慢一些,可是影響並不大。

與垃圾回收算法相關的PHP配置

能夠經過修改php.ini中的zend.enable_gc來打開或關閉PHP的垃圾回收機制,也能夠經過調用gc_enable()或gc_disable()打開或關閉PHP的垃圾回收機制。在PHP5.3中即便關閉了垃圾回收機制,PHP仍然會記錄可能根到根緩衝區,只是當根緩衝區滿額時,PHP不會自動運行垃圾回收,固然,任什麼時候候您均可以經過手工調用gc_collect_cycles()函數強制執行內存回收。

相關文章
相關標籤/搜索