PHP垃圾回收機(jī)制

垃圾的產(chǎn)生

之前的文章已經(jīng)介紹過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)過程。

但是在循環(huán)引用中,是無法通過這一機(jī)制回收變量的。即當(dāng)數(shù)組或?qū)ο髢?nèi)部子元素引用其父元素,而此時(shí)如果發(fā)生了刪除其父元素的情況,此變量容器并不會(huì)被刪除,因?yàn)閿?shù)組的引用計(jì)數(shù)中就有一個(gè)來自自身成員,試圖釋放數(shù)組時(shí)因?yàn)槠鋜efcount仍然大于0而得不到釋放,而實(shí)際上已經(jīng)沒有任何外部引用了,所以無法被清除,因此會(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í)是無法通過正常的gc機(jī)制回收的,但是$a已經(jīng)已經(jīng)沒有任何外部引用了,所以這種變量就是垃圾,垃圾回收器要處理的就是這種情況,這里明確兩個(gè)準(zhǔn)則:
1.如果一個(gè)變量value的refcount減少到0, 那么此value可以被釋放掉,不屬于垃圾
2.如果一個(gè)變量value的refcount減少之后大于0,那么此zval還不能被釋放,此zval可能成為一個(gè)垃圾

針對(duì)第一個(gè)情況GC不會(huì)處理,只有第二種情況GC才會(huì)將變量收集起來。另外變量是否加入垃圾檢查buffer并不是根據(jù)zval的類型判斷的,是通過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ì)處理這兩種類型的變量。

回收過程

如果當(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信息,垃圾回收的過程會(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)加入過了則不再重復(fù)插入。

垃圾緩存區(qū)是一個(gè)雙向鏈表,等到緩存區(qū)滿了以后則啟動(dòng)垃圾檢查過程:遍歷緩存區(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)榱?則就表明其引用全部來自自身成員。具體的過程如下:

1.從buffer鏈表的roots開始遍歷,把當(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則排除了引用全部來自自身成員的可能,表示還有外部的引用,并不是垃圾,這時(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)

接下來我們簡(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;  //是否在垃圾檢查過程中
    zend_bool         gc_full;    //緩存區(qū)是否已滿

    gc_root_buffer   *buf;   //啟動(dòng)時(shí)分配的用于保存可能垃圾的緩存區(qū)
    gc_root_buffer    roots; //指向buf中最新加入的一個(gè)可能垃圾
    gc_root_buffer   *unused;//指向buf中沒有使用的buffer
    gc_root_buffer   *first_unused; //指向buf中第一個(gè)沒有使用的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是垃圾回收過程中主要用到的一個(gè)結(jié)構(gòu),用來保存垃圾回收器的所有信息,比如垃圾緩存區(qū);gc_root_buffer用來保存每個(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)說過,當(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檢查的過程就是從roots開始遍歷的;

3.first_unused: 指向buf中第一個(gè)可用的節(jié)點(diǎn),初始化時(shí)這個(gè)值為1而不是0,因?yàn)榈谝粋€(gè)gc_root_buffer保留沒有使用,有元素插入roots時(shí)如果first_unused還沒有到達(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)一些開始加入后面又刪除的節(jié)點(diǎn),這些節(jié)點(diǎn)就通過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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容