前言PHP是一門托管型語言在PHP編程中程序員不需要手工處理內存資源的分配與釋放(使用C編寫PHP或Zend擴展除外)這就意味著PHP本身實現了垃圾回收機制(Garbage Collection)現在如果去PHP官方網站(phpnet)可以看到目前PHP的兩個分支版本PHP和PHP是分別更新的這是因為許多項目仍然使用版本的PHP而版本對並不是完全兼容PHP在PHP的基礎上做了諸多改進其中垃圾回收算法就屬於一個比較大的改變本文將分別討論PHP和PHP的垃圾回收機制並討論這種演化和改進對於程序員編寫PHP的影響以及要注意的問題
PHP變量及關聯內存對象的內部表示
垃圾回收說到底是對變量及其所關聯內存對象的操作所以在討論PHP的垃圾回收機制之前先簡要介紹PHP中變量及其內存對象的內部表示(其C源代碼中的表示)
PHP官方文檔中將PHP中的變量劃分為兩類標量類型和復雜類型標量類型包括布爾型整型浮點型和字符串復雜類型包括數組對象和資源還有一個NULL比較特殊它不劃分為任何類型而是單獨成為一類
所有這些類型在PHP內部統一用一個叫做zval的結構表示在PHP源代碼中這個結構名稱為“_zval_struct”zval的具體定義在PHP源代碼的“Zend/zendh”文件中下面是相關代碼的摘錄
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中只有個字段但是PHP中算上NULL有種數據類型那麼PHP內部是如何用個字段表示種類型呢?這算是PHP設計比較巧妙的一個地方它通過復用字段達到了減少字段的目的例如在PHP內部布爾型整型及資源(只要存儲資源的標識符即可)都是通過lval字段存儲的dval用於存儲浮點型str存儲字符串ht存儲數組(注意PHP中的數組其實是哈希表)而obj存儲對象類型如果所有字段全部置為或NULL則表示PHP中的NULL這樣就達到了用個字段存儲種類型的值
而當前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的“WriteOnCopy”機制有關由於這個話題不是本文重點因此這裡不再詳述讀者只需記住refcount__gc這個字段的作用即可
PHP中的垃圾回收算法——Reference Counting
PHP中使用的內存回收算法是大名鼎鼎的Reference Counting這個算法中文翻譯叫做“引用計數”其思想非常直觀和簡潔為每個內存對象分配一個計數器當一個內存對象建立時計數器初始化為(因此此時總是有一個變量引用此對象)以後每有一個新變量引用此內存對象則計數器加而每當減少一個引用此內存對象的變量則計數器減當垃圾回收機制運作的時候將所有計數器為的內存對象銷毀並回收其占用的內存而PHP中內存對象就是zval而計數器就是refcount__gc
例如下面一段PHP代碼演示了PHP計數器的工作原理(計數器值通過xdebugorg得到)
<?php
$val = ; //zval(val)refcount_gc = ;
$val = $val; //zval(val)refcount_gc = zval(val)refcount_gc = (因為是Write on copy當前val與val共同引用一個zval)
$val = ; //zval(val)refcount_gc = zval(val)refcount_gc = (此處val新建了一個zval)
unset($val); //zval(val)refcount_gc = ($val引用的zval再也不可用會被GC回收)
?>
Reference Counting簡單直觀實現方便但卻存在一個致命的缺陷就是容易造成內存洩露很多朋友可能已經意識到了如果存在循環引用那麼Reference Counting就可能導致內存洩露例如下面的代碼
<?php
$a = array();
$a[] = & $a;
unset($a);
?>
這段代碼首先建立了數組a然後讓a的第一個元素按引用指向a這時a的zval的refcount就變為然後我們銷毀變量a此時a最初指向的zval的refcount為但是我們再也沒有辦法對其進行操作因為其形成了一個循環自引用如下圖所示
其中灰色部分表示已經不復存在由於a之前指向的zval的refcount為(被其HashTable的第一個元素引用)這個zval就不會被GC銷毀這部分內存就洩露了
這裡特別要指出的是PHP是通過符號表(Symbol Table)存儲變量符號的全局有一個符號表而每個復雜類型如數組或對象有自己的符號表因此上面代碼中a和a[]是兩個符號但是a儲存在全局符號表中而a[]儲存在數組本身的符號表中且這裡a和a[]引用同一個zval(當然符號a後來被銷毀了)希望讀者朋友注意分清符號(Symbol)的zval的關系
在PHP只用於做動態頁面腳本時這種洩露也許不是很要緊因為動態頁面腳本的生命周期很短PHP會保證當腳本執行完畢後釋放其所有資源但是PHP發展到目前已經不僅僅用作動態頁面腳本這麼簡單如果將PHP用在生命周期較長的場景中例如自動化測試腳本或deamon進程那麼經過多次循環後積累下來的內存洩露可能就會很嚴重這並不是我在聳人聽聞我曾經實習過的一個公司就通過PHP寫的deamon進程來與數據存儲服務器交互
由於Reference Counting的這個缺陷PHP改進了垃圾回收算法
PHP中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems
PHP的垃圾回收算法仍然以引用計數為基礎但是不再是使用簡單計數作為回收准則而是使用了一種同步回收算法這個算法由IBM的工程師在論文Concurrent Cycle Collection in Reference Counted Systems中提出
這個算法可謂相當復雜從論文頁的數量我想大家也能看出來所以我不打算(也沒有能力)完整論述此算法有興趣的朋友可以閱讀上面的提到的論文(強烈推薦這篇論文非常精彩)
我在這裡只能大體描述一下此算法的基本思想
首先PHP會分配一個固定大小的“根緩沖區”這個緩沖區用於存放固定數量的zval這個數量默認是如果需要修改則需要修改源代碼Zend/zend_gcc中的常量GC_ROOT_BUFFER_MAX_ENTRIES然後重新編譯
由上文我們可以知道一個zval如果有引用要麼被全局符號表中的符號引用要麼被其它表示復雜類型的zval中的符號引用因此在zval中存在一些可能根(root)這裡我們暫且不討論PHP是如何發現這些可能根的這是個很復雜的問題總之PHP有辦法發現這些可能根zval並將它們投入根緩沖區
當根緩沖區滿額時PHP就會執行垃圾回收此回收算法如下
對每個根緩沖區中的根zval按照深度優先遍歷算法遍歷所有能遍歷到的zval並將每個zval的refcount減同時為了避免對同一zval多次減(因為可能不同的根能遍歷到同一個zval)每次對某個zval減後就對其標記為“已減”
再次對每個緩沖區中的根zval深度優先遍歷如果某個zval的refcount不為則對其加否則保持其為
清空根緩沖區中的所有根(注意是把這些zval從緩沖區中清除而不是銷毀它們)然後銷毀所有refcount為的zval並收回其內存
如果不能完全理解也沒有關系只需記住PHP的垃圾回收算法有以下幾點特性
並不是每次refcount減少時都進入回收周期只有根緩沖區滿額後在開始垃圾回收
可以解決循環引用問題
可以總將內存洩露保持在一個阈值以下
PHP與PHP垃圾回收算法的性能比較
由於我目前條件所限我就不重新設計試驗了而是直接引用PHP Manual中的實驗關於兩者的性能比較請參考PHP Manual中的相關章節
首先是內存洩露試驗下面直接引用PHP Manual中的實驗代碼和試驗結果圖
<?php
class Foo
{
public $var = ;
}
$baseMemory = memory_get_usage();
for ( $i = ; $i <= ; $i++ )
{
$a = new Foo;
$a>self = $a;
if ( $i % === )
{
echo sprintf( %d: $i ) memory_get_usage() $baseMemory "n";
}
}
?>
可以看到在可能引發累積性內存洩露的場景下PHP發生持續累積性內存洩露而PHP則總能將內存洩露控制在一個阈值以下(與根緩沖區大小有關)
另外是關於性能方面的對比
<?php
class Foo
{
public $var = ;
}
for ( $i = ; $i <= ; $i++ )
{
$a = new Foo;
$a>self = $a;
}
echo memory_get_peak_usage() "n";
?>
這個腳本執行次循環使得延遲時間足夠進行對比然後使用CLI方式分別在打開內存回收和關閉內存回收的的情況下運行此腳本
time php dzendenable_gc= dmemory_limit= n examplephp
# and
time php dzendenable_gc= dmemory_limit= n examplephp
在我的機器環境下運行時間分別為s和s可以看到PHP的垃圾回收機制會慢一些但是影響並不大
與垃圾回收算法相關的PHP配置
可以通過修改phpini中的zendenable_gc來打開或關閉PHP的垃圾回收機制也可以通過調用gc_enable( )或gc_disable( )打開或關閉PHP的垃圾回收機制在PHP中即使關閉了垃圾回收機制PHP仍然會記錄可能根到根緩沖區只是當根緩沖區滿額時PHP不會自動運行垃圾回收當然任何時候您都可以通過手工調用gc_collect_cycles( )函數強制執行內存回收
From:http://tw.wingwit.com/Article/program/PHP/201311/21185.html