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

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


前言

PHP是一門托管型語言,在PHP編程中程序員不需要手工處理內存資源的分配與釋放(使用C編寫PHP或Zend擴展除外)鸟赫,這就意味著PHP本身實現(xiàn)了垃圾回收機制(Garbage Collection)。現(xiàn)在如果去PHP官方網(wǎng)站(php.net)可以看到坤候,目前PHP5的兩個分支版本PHP5.2和PHP5.3是分別更新的吨拗,這是因為許多項目仍然使用5.2版本的PHP箕慧,而5.3版本對5.2并不是完全兼容。PHP5.3在PHP5.2的基礎上做了諸多改進献联,其中垃圾回收算法就屬于一個比較大的改變竖配。本文將分別討論PHP5.2和PHP5.3的垃圾回收機制,并討論這種演化和改進對于程序員編寫PHP的影響以及要注意的問題里逆。

PHP變量及關聯(lián)內存對象的內部表示

垃圾回收說到底是對變量及其所關聯(lián)內存對象的操作进胯,所以在討論PHP的垃圾回收機制之前,先簡要介紹PHP中變量及其內存對象的內部表示(其C源代碼中的表示)原押。

PHP官方文檔中將PHP中的變量劃分為兩類:標量類型和復雜類型胁镐。標量類型包括布爾型、整型诸衔、浮點型和字符串盯漂;復雜類型包括數(shù)組、對象和資源笨农;還有一個NULL比較特殊就缆,它不劃分為任何類型,而是單獨成為一類谒亦。

所有這些類型违崇,在PHP內部統(tǒng)一用一個叫做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;
};

其中聯(lián)合體“_zvalue_value”用于表示PHP中所有變量的值脾还,這里之所以使用union,是因為一個zval在一個時刻只能表示一種類型的變量入愧”陕可以看到_zvalue_value中只有5個字段,但是PHP中算上NULL有8種數(shù)據(jù)類型棺蛛,那么PHP內部是如何用5個字段表示8種類型呢怔蚌?這算是PHP設計比較巧妙的一個地方,它通過復用字段達到了減少字段的目的旁赊。例如桦踊,在PHP內部布爾型、整型及資源(只要存儲資源的標識符即可)都是通過lval字段存儲的终畅;dval用于存儲浮點型籍胯;str存儲字符串竟闪;ht存儲數(shù)組(注意PHP中的數(shù)組其實是哈希表);而obj存儲對象類型杖狼;如果所有字段全部置為0或NULL則表示PHP中的NULL炼蛤,這樣就達到了用5個字段存儲8種類型的值。

而當前zval中的value(value的類型即是_zvalue_value)到底表示那種類型蝶涩,則由“_zval_struct”中的type確定理朋。_zval_struct即是zval在C語言中的具體實現(xiàn),每個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土思,這個算法中文翻譯叫做“引用計數(shù)”,其思想非常直觀和簡潔:為每個內存對象分配一個計數(shù)器忆嗜,當一個內存對象建立時計數(shù)器初始化為1(因此此時總是有一個變量引用此對象)己儒,以后每有一個新變量引用此內存對象,則計數(shù)器加1捆毫,而每當減少一個引用此內存對象的變量則計數(shù)器減1闪湾,當垃圾回收機制運作的時候,將所有計數(shù)器為0的內存對象銷毀并回收其占用的內存绩卤。而PHP中內存對象就是zval途样,而計數(shù)器就是refcount__gc。

例如下面一段PHP代碼演示了PHP5.2計數(shù)器的工作原理(計數(shù)器值通過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簡單直觀,實現(xiàn)方便凛驮,但卻存在一個致命的缺陷裆站,就是容易造成內存泄露。很多朋友可能已經(jīng)意識到了,如果存在循環(huán)引用遏插,那么Reference Counting就可能導致內存泄露捂贿。例如下面的代碼:

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

這段代碼首先建立了數(shù)組a,然后讓a的第一個元素按引用指向a胳嘲,這時a的zval的refcount就變?yōu)?厂僧,然后我們銷毀變量a,此時a最初指向的zval的refcount為1了牛,但是我們再也沒有辦法對其進行操作颜屠,因為其形成了一個循環(huán)自引用,如下圖所示:

其中灰色部分表示已經(jīng)不復存在鹰祸。由于a之前指向的zval的refcount為1(被其HashTable的第一個元素引用)甫窟,這個zval就不會被GC銷毀,這部分內存就泄露了蛙婴。

這里特別要指出的是粗井,PHP是通過符號表(Symbol Table)存儲變量符號的,全局有一個符號表街图,而每個復雜類型如數(shù)組或對象有自己的符號表浇衬,因此上面代碼中,a和a[0]是兩個符號餐济,但是a儲存在全局符號表中耘擂,而a[0]儲存在數(shù)組本身的符號表中,且這里a和a[0]引用同一個zval(當然符號a后來被銷毀了)絮姆。希望讀者朋友注意分清符號(Symbol)的zval的關系醉冤。

在PHP只用于做動態(tài)頁面腳本時,這種泄露也許不是很要緊篙悯,因為動態(tài)頁面腳本的生命周期很短蚁阳,PHP會保證當腳本執(zhí)行完畢后,釋放其所有資源鸽照。但是PHP發(fā)展到目前已經(jīng)不僅僅用作動態(tài)頁面腳本這么簡單韵吨,如果將PHP用在生命周期較長的場景中,例如自動化測試腳本或deamon進程移宅,那么經(jīng)過多次循環(huán)后積累下來的內存泄露可能就會很嚴重。這并不是我在聳人聽聞椿疗,我曾經(jīng)實習過的一個公司就通過PHP寫的deamon進程來與數(shù)據(jù)存儲服務器交互漏峰。

由于Reference Counting的這個缺陷,PHP5.3改進了垃圾回收算法届榄。

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

PHP5.3的垃圾回收算法仍然以引用計數(shù)為基礎浅乔,但是不再是使用簡單計數(shù)作為回收準則,而是使用了一種同步回收算法,這個算法由IBM的工程師在論文Concurrent Cycle Collection in Reference Counted Systems中提出靖苇。

這個算法可謂相當復雜席噩,從論文29頁的數(shù)量我想大家也能看出來,所以我不打算(也沒有能力)完整論述此算法贤壁,有興趣的朋友可以閱讀上面的提到的論文(強烈推薦悼枢,這篇論文非常精彩)。

我在這里脾拆,只能大體描述一下此算法的基本思想馒索。

首先PHP會分配一個固定大小的“根緩沖區(qū)”,這個緩沖區(qū)用于存放固定數(shù)量的zval名船,這個數(shù)量默認是10,000绰上,如果需要修改則需要修改源代碼Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新編譯。

由上文我們可以知道渠驼,一個zval如果有引用蜈块,要么被全局符號表中的符號引用,要么被其它表示復雜類型的zval中的符號引用迷扇。因此在zval中存在一些可能根(root)百揭。這里我們暫且不討論PHP是如何發(fā)現(xiàn)這些可能根的,這是個很復雜的問題谋梭,總之PHP有辦法發(fā)現(xiàn)這些可能根zval并將它們投入根緩沖區(qū)信峻。

當根緩沖區(qū)滿額時,PHP就會執(zhí)行垃圾回收瓮床,此回收算法如下:

1盹舞、對每個根緩沖區(qū)中的根zval按照深度優(yōu)先遍歷算法遍歷所有能遍歷到的zval,并將每個zval的refcount減1隘庄,同時為了避免對同一zval多次減1(因為可能不同的根能遍歷到同一個zval)踢步,每次對某個zval減1后就對其標記為“已減”。

2丑掺、再次對每個緩沖區(qū)中的根zval深度優(yōu)先遍歷获印,如果某個zval的refcount不為0,則對其加1街州,否則保持其為0兼丰。

3、清空根緩沖區(qū)中的所有根(注意是把這些zval從緩沖區(qū)中清除而不是銷毀它們)唆缴,然后銷毀所有refcount為0的zval鳍征,并收回其內存。

如果不能完全理解也沒有關系面徽,只需記住PHP5.3的垃圾回收算法有以下幾點特性:

1艳丛、并不是每次refcount減少時都進入回收周期匣掸,只有根緩沖區(qū)滿額后在開始垃圾回收。

2氮双、可以解決循環(huán)引用問題碰酝。

3、可以總將內存泄露保持在一個閾值以下戴差。

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

由于我目前條件所限送爸,我就不重新設計試驗了,而是直接引用PHP Manual中的實驗造挽,關于兩者的性能比較請參考PHP Manual中的相關章節(jié):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";
    }
}
?>
image

可以看到在可能引發(fā)累積性內存泄露的場景下饭入,PHP5.2發(fā)生持續(xù)累積性內存泄露嵌器,而PHP5.3則總能將內存泄露控制在一個閾值以下(與根緩沖區(qū)大小有關)。

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

<?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";
?>

這個腳本執(zhí)行1000000次循環(huán)谐丢,使得延遲時間足夠進行對比爽航。

然后使用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

在我的機器環(huán)境下,運行時間分別為6.4s和7.2s乾忱,可以看到PHP5.3的垃圾回收機制會慢一些讥珍,但是影響并不大。

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

可以通過修改php.ini中的zend.enable_gc來打開或關閉PHP的垃圾回收機制窄瘟,也可以通過調用gc_enable()或gc_disable()打開或關閉PHP的垃圾回收機制衷佃。在PHP5.3中即使關閉了垃圾回收機制,PHP仍然會記錄可能根到根緩沖區(qū)蹄葱,只是當根緩沖區(qū)滿額時氏义,PHP不會自動運行垃圾回收,當然图云,任何時候您都可以通過手工調用gc_collect_cycles()函數(shù)強制執(zhí)行內存回收惯悠。

轉自:http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市竣况,隨后出現(xiàn)的幾起案子克婶,更是在濱河造成了極大的恐慌,老刑警劉巖丹泉,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件情萤,死亡現(xiàn)場離奇詭異,居然都是意外死亡摹恨,警方通過查閱死者的電腦和手機筋岛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來睬塌,“玉大人,你說我怎么就攤上這事】纾” “怎么了勋陪?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長硫兰。 經(jīng)常有香客問我诅愚,道長,這世上最難降的妖魔是什么劫映? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任违孝,我火速辦了婚禮,結果婚禮上泳赋,老公的妹妹穿的比我還像新娘雌桑。我一直安慰自己,他們只是感情好祖今,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布校坑。 她就那樣靜靜地躺著,像睡著了一般千诬。 火紅的嫁衣襯著肌膚如雪耍目。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天徐绑,我揣著相機與錄音邪驮,去河邊找鬼。 笑死傲茄,一個胖子當著我的面吹牛毅访,可吹牛的內容都是我干的。 我是一名探鬼主播烫幕,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼俺抽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了较曼?” 一聲冷哼從身側響起磷斧,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捷犹,沒想到半個月后弛饭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡萍歉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年侣颂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枪孩。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡憔晒,死狀恐怖藻肄,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情拒担,我是刑警寧澤嘹屯,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站从撼,受9級特大地震影響州弟,放射性物質發(fā)生泄漏。R本人自食惡果不足惜低零,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一婆翔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掏婶,春花似錦啃奴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茎芭,卻和暖如春揖膜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梅桩。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工壹粟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宿百。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓趁仙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親垦页。 傳聞我的和親對象是個殘疾皇子雀费,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容