PHP垃圾回收機(jī)制

垃圾的產(chǎn)生

之前的文章已經(jīng)介紹過(guò)PHP的引用計(jì)數(shù)機(jī)制-PHP內(nèi)核探索之變量-理解引用嚣镜,當(dāng)變量賦值、傳遞時(shí)并不會(huì)直接硬拷貝豆同,而是增加value的引用數(shù),unset含鳞、return等釋放變量時(shí)再減掉引用數(shù)影锈,減掉后如果發(fā)現(xiàn)refcount變?yōu)?則直接釋放value,這是變量的基本GC(Garbage Collection)過(guò)程。

但是在循環(huán)引用中鸭廷,是無(wú)法通過(guò)這一機(jī)制回收變量的枣抱。即當(dāng)數(shù)組或?qū)ο髢?nèi)部子元素引用其父元素,而此時(shí)如果發(fā)生了刪除其父元素的情況辆床,此變量容器并不會(huì)被刪除佳晶,因?yàn)閿?shù)組的引用計(jì)數(shù)中就有一個(gè)來(lái)自自身成員,試圖釋放數(shù)組時(shí)因?yàn)槠鋜efcount仍然大于0而得不到釋放讼载,而實(shí)際上已經(jīng)沒(méi)有任何外部引用了轿秧,所以無(wú)法被清除,因此會(huì)發(fā)生內(nèi)存泄漏咨堤。
下面看一個(gè)數(shù)組循環(huán)引用的例子:

$a = array( 'one' );
$a[] = &$a;

unset($a);

unset($a)之前的引用關(guān)系:

image.png

unset($a)之后的引用關(guān)系:


image.png

可以看到菇篡,unset(a)之后由于數(shù)組中有子元素指向 a,所以refcount = 1一喘,此時(shí)是無(wú)法通過(guò)正常的gc機(jī)制回收的驱还,但是$a已經(jīng)已經(jīng)沒(méi)有任何外部引用了,所以這種變量就是垃圾凸克,垃圾回收器要處理的就是這種情況议蟆,這里明確兩個(gè)準(zhǔn)則:
1.如果一個(gè)變量value的refcount減少到0, 那么此value可以被釋放掉萎战,不屬于垃圾
2.如果一個(gè)變量value的refcount減少之后大于0咪鲜,那么此zval還不能被釋放,此zval可能成為一個(gè)垃圾

針對(duì)第一個(gè)情況GC不會(huì)處理撞鹉,只有第二種情況GC才會(huì)將變量收集起來(lái)疟丙。另外變量是否加入垃圾檢查buffer并不是根據(jù)zval的類型判斷的,是通過(guò)zval.u1.type_flag記錄的鸟雏,只有包含IS_TYPE_COLLECTABLE的變量才會(huì)被GC收集享郊。

目前垃圾只會(huì)出現(xiàn)在array、object兩種類型中孝鹊,數(shù)組的情況上面已經(jīng)介紹了炊琉,object的情況則是成員屬性引用對(duì)象本身導(dǎo)致的,其它類型不會(huì)出現(xiàn)這種變量中的成員引用變量自身的情況又活,所以垃圾回收只會(huì)處理這兩種類型的變量苔咪。

回收過(guò)程

如果當(dāng)變量的refcount減少后大于0,PHP并不會(huì)立即進(jìn)行對(duì)這個(gè)變量進(jìn)行垃圾鑒定柳骄,而是放入一個(gè)緩沖buffer中团赏,等這個(gè)buffer滿了以后(默認(rèn)10000個(gè)值)再統(tǒng)一進(jìn)行處理,加入buffer的是變量zend_value的zend_refcounted_h:

typedef struct _zend_refcounted_h {
    uint32_t         refcount; //記錄zend_value的引用數(shù)
    union {
        struct {
            zend_uchar    type,  //zend_value的類型,與zval.u1.type一致
            zend_uchar    flags, 
            uint16_t      gc_info //GC信息耐薯,垃圾回收的過(guò)程會(huì)用到
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

一個(gè)變量只能加入一次buffer舔清,為了防止重復(fù)加入丝里,變量加入后會(huì)把zend_refcounted_h.gc_info置為GC_PURPLE,即標(biāo)為紫色体谒,下次refcount減少時(shí)如果發(fā)現(xiàn)已經(jīng)加入過(guò)了則不再重復(fù)插入杯聚。

垃圾緩存區(qū)是一個(gè)雙向鏈表,等到緩存區(qū)滿了以后則啟動(dòng)垃圾檢查過(guò)程:遍歷緩存區(qū)抒痒,再對(duì)當(dāng)前變量的所有成員進(jìn)行遍歷幌绍,然后把成員的refcount減1(如果成員還包含子成員則也進(jìn)行遞歸遍歷,其實(shí)就是深度優(yōu)先的遍歷)故响,最后再檢查當(dāng)前變量的引用傀广,如果減為了0則為垃圾。這個(gè)算法的原理很簡(jiǎn)單被去,垃圾是由于成員引用自身導(dǎo)致的主儡,那么就對(duì)所有的成員減一遍引用,結(jié)果如果發(fā)現(xiàn)變量本身refcount變?yōu)榱?則就表明其引用全部來(lái)自自身成員惨缆。具體的過(guò)程如下:

1.從buffer鏈表的roots開(kāi)始遍歷糜值,把當(dāng)前value標(biāo)為灰色(zend_refcounted_h.gc_info置為GC_GREY),然后對(duì)當(dāng)前value的成員進(jìn)行深度優(yōu)先遍歷坯墨,把成員value的refcount減1寂汇,并且也標(biāo)為灰色;
2.重復(fù)遍歷buffer鏈表捣染,檢查當(dāng)前value引用是否為0骄瓣,為0則表示確實(shí)是垃圾,把它標(biāo)為白色(GC_WHITE)耍攘,如果不為0則排除了引用全部來(lái)自自身成員的可能榕栏,表示還有外部的引用,并不是垃圾蕾各,這時(shí)候因?yàn)椴襟E(1)對(duì)成員進(jìn)行了refcount減1操作扒磁,需要再還原回去,對(duì)所有成員進(jìn)行深度遍歷式曲,把成員refcount加1妨托,同時(shí)標(biāo)為黑色;
3.再次遍歷buffer鏈表吝羞,將非GC_WHITE的節(jié)點(diǎn)從roots鏈表中刪除兰伤,最終roots鏈表中全部為真正的垃圾,最后將這些垃圾清除钧排。

垃圾收集的內(nèi)部實(shí)現(xiàn)

接下來(lái)我們簡(jiǎn)單看下垃圾回收的內(nèi)部實(shí)現(xiàn)敦腔,垃圾收集器的全局?jǐn)?shù)據(jù)結(jié)構(gòu):

typedef struct _zend_gc_globals {
    zend_bool         gc_enabled; //是否啟用gc
    zend_bool         gc_active;  //是否在垃圾檢查過(guò)程中
    zend_bool         gc_full;    //緩存區(qū)是否已滿

    gc_root_buffer   *buf;   //啟動(dòng)時(shí)分配的用于保存可能垃圾的緩存區(qū)
    gc_root_buffer    roots; //指向buf中最新加入的一個(gè)可能垃圾
    gc_root_buffer   *unused;//指向buf中沒(méi)有使用的buffer
    gc_root_buffer   *first_unused; //指向buf中第一個(gè)沒(méi)有使用的buffer
    gc_root_buffer   *last_unused; //指向buf尾部

    gc_root_buffer    to_free;  //待釋放的垃圾
    gc_root_buffer   *next_to_free;

    uint32_t gc_runs;   //統(tǒng)計(jì)gc運(yùn)行次數(shù)
    uint32_t collected; //統(tǒng)計(jì)已回收的垃圾數(shù)
} zend_gc_globals;

typedef struct _gc_root_buffer {
    zend_refcounted          *ref; //每個(gè)zend_value的gc信息
    struct _gc_root_buffer   *next;
    struct _gc_root_buffer   *prev;
    uint32_t                 refcount;
} gc_root_buffer;

zend_gc_globals是垃圾回收過(guò)程中主要用到的一個(gè)結(jié)構(gòu),用來(lái)保存垃圾回收器的所有信息卖氨,比如垃圾緩存區(qū)会烙;gc_root_buffer用來(lái)保存每個(gè)可能是垃圾的變量负懦,它實(shí)際就是整個(gè)垃圾收集buffer鏈表的元素筒捺,當(dāng)GC收集一個(gè)變量時(shí)會(huì)創(chuàng)建一個(gè)gc_root_buffer柏腻,插入鏈表。

zend_gc_globals這個(gè)結(jié)構(gòu)中有幾個(gè)關(guān)鍵成員:
1.buf: 前面已經(jīng)說(shuō)過(guò)系吭,當(dāng)refcount減少后如果大于0那么就會(huì)將這個(gè)變量的value加入GC的垃圾緩存區(qū)五嫂,buf就是這個(gè)緩存區(qū),它實(shí)際是一塊連續(xù)的內(nèi)存肯尺,在GC初始化時(shí)一次性分配了10001個(gè)gc_root_buffer沃缘,插入變量時(shí)直接從buf中取出可用節(jié)點(diǎn);

2.roots: 垃圾緩存鏈表的頭部则吟,啟動(dòng)GC檢查的過(guò)程就是從roots開(kāi)始遍歷的槐臀;

3.first_unused: 指向buf中第一個(gè)可用的節(jié)點(diǎn),初始化時(shí)這個(gè)值為1而不是0氓仲,因?yàn)榈谝粋€(gè)gc_root_buffer保留沒(méi)有使用水慨,有元素插入roots時(shí)如果first_unused還沒(méi)有到達(dá)buf的尾部則返回first_unused給最新的元素,然后first_unused++敬扛,直到last_unused晰洒,比如現(xiàn)在已經(jīng)加入了2個(gè)可能的垃圾變量,則對(duì)應(yīng)的結(jié)構(gòu):

image.png

4.last_unused: 與first_unused類似啥箭,指向buf末尾

5.unused: GC收集變量時(shí)會(huì)依次從buf中獲取可用的gc_root_buffer谍珊,這種情況直接取first_unused即可,但是有些變量加入垃圾緩存區(qū)之后其refcount又減為0了急侥,這種情況就需要從roots中刪掉砌滞,因?yàn)樗豢赡苁抢@樣就導(dǎo)致roots鏈表并不是像buf分配的那樣是連續(xù)的坏怪,中間會(huì)出現(xiàn)一些開(kāi)始加入后面又刪除的節(jié)點(diǎn)贝润,這些節(jié)點(diǎn)就通過(guò)unused串成一個(gè)單鏈表,unused指向鏈表尾部陕悬,下次有新的變量插入roots時(shí)優(yōu)先使用unused的這些節(jié)點(diǎn)题暖,其次才是first_unused的,舉個(gè)例子

//示例1:
$a = array(); //$a ->  zend_array(refcount=1)
$b = $a;      //$a ->  zend_array(refcount=2)
              //$b ->

unset($b);    //此時(shí)zend_array(refcount=1)捉超,因?yàn)閞efoucnt>0所以加入gc的垃圾緩存區(qū):roots
unset($a);    //此時(shí)zend_array(refcount=0)且gc_info為GC_PURPLE胧卤,則從roots鏈表中刪掉

假如unset($b)時(shí)插入的是buf中第1個(gè)位置,那么unset(a)后對(duì)應(yīng)的結(jié)構(gòu):


image.png

如果后面再有變量加入GC垃圾緩存區(qū)將優(yōu)先使用第1個(gè)拼岳。

整理自---《PHP7內(nèi)核剖析》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末枝誊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子惜纸,更是在濱河造成了極大的恐慌叶撒,老刑警劉巖绝骚,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異祠够,居然都是意外死亡压汪,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)古瓤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)止剖,“玉大人,你說(shuō)我怎么就攤上這事落君〈┫悖” “怎么了污它?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵猾普,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我瞄桨,道長(zhǎng)纹冤,這世上最難降的妖魔是什么洒宝? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮赵哲,結(jié)果婚禮上待德,老公的妹妹穿的比我還像新娘。我一直安慰自己枫夺,他們只是感情好将宪,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著橡庞,像睡著了一般较坛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扒最,一...
    開(kāi)封第一講書(shū)人閱讀 49,829評(píng)論 1 290
  • 那天丑勤,我揣著相機(jī)與錄音,去河邊找鬼吧趣。 笑死法竞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的强挫。 我是一名探鬼主播岔霸,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼俯渤!你這毒婦竟也來(lái)了呆细?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤八匠,失蹤者是張志新(化名)和其女友劉穎絮爷,沒(méi)想到半個(gè)月后趴酣,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坑夯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年岖寞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渊涝。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慎璧,死狀恐怖床嫌,靈堂內(nèi)的尸體忽然破棺而出跨释,到底是詐尸還是另有隱情,我是刑警寧澤厌处,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布鳖谈,位于F島的核電站,受9級(jí)特大地震影響阔涉,放射性物質(zhì)發(fā)生泄漏缆娃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一瑰排、第九天 我趴在偏房一處隱蔽的房頂上張望贯要。 院中可真熱鬧,春花似錦椭住、人聲如沸崇渗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宅广。三九已至,卻和暖如春些举,著一層夾襖步出監(jiān)牢的瞬間跟狱,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工户魏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驶臊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓叼丑,卻偏偏與公主長(zhǎng)得像关翎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子幢码,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349