php7垃圾回收機(jī)制

0. gc的基本結(jié)構(gòu)

0.1 zend_refcounted_h

《php7的引用計(jì)數(shù)》一文中掸茅,我們說過父腕,php7的復(fù)雜類型弱匪,像字符串、數(shù)組璧亮、引用等的數(shù)據(jù)結(jié)構(gòu)中萧诫,頭部都有一個(gè)gc,變量的引用計(jì)數(shù)維護(hù)在這個(gè)gc中枝嘶。gc是zend_refcounted_h類型的帘饶,其定義如下:

//php7.0 Zend/zend_types.h

typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type, 
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

struct _zend_refcounted {
    zend_refcounted_h gc;
};

typedef struct _zend_refcounted zend_refcounted;

zend_refcounted是由uint32_t的refcount和uint32_t的type_info組成的,總大小為8字節(jié)群扶。type_info中的4字節(jié)(每個(gè)字節(jié)8bit)有著各自的意義及刻,分別如下:

  • type:當(dāng)前元素的類型,同zval的u1.v.type竞阐。(為何要冗余記錄一份提茁,我們?cè)诘?部分講解。)
  • flags:標(biāo)記數(shù)據(jù)類型馁菜,可以是字符串類型或數(shù)組類型等茴扁。其中標(biāo)記字符串的flags有:
php7.0 /Zend/zend_types.h
/* string flags (zval.value->gc.u.flags) */
#define IS_STR_PERSISTENT           (1<<0) /* allocated using malloc   */
#define IS_STR_INTERNED             (1<<1) /* interned string          */
#define IS_STR_PERMANENT            (1<<2) /* relives request boundary */
#define IS_STR_CONSTANT             (1<<3) /* constant index */
#define IS_STR_CONSTANT_UNQUALIFIED (1<<4) /* the same as IS_CONSTANT_UNQUALIFIED */

標(biāo)記數(shù)組的flags有:

/* array flags */
#define IS_ARRAY_IMMUTABLE          (1<<1) /* the same as IS_TYPE_IMMUTABLE */

標(biāo)記對(duì)象的flags有:

/* object flags (zval.value->gc.u.flags) */
#define IS_OBJ_APPLY_COUNT          0x07
#define IS_OBJ_DESTRUCTOR_CALLED    (1<<3)
#define IS_OBJ_FREE_CALLED          (1<<4)
#define IS_OBJ_USE_GUARDS           (1<<5)
#define IS_OBJ_HAS_GUARDS           (1<<6)
  • gc_info:后面的兩個(gè)字節(jié)標(biāo)記當(dāng)前元素的顏色和垃圾回收池中的位置,其中高地址的兩位用來標(biāo)記顏色汪疮,低地址的14位用于記錄位置峭火。源碼中定義垃圾回收池的大小為100001, 14位可以表示0~16383(2^14-1)智嚷,足夠定義其在回收池中的位置卖丸。
    源碼中定義的顏色如下:
//php7.0 Zend/zend_gc.h
#define GC_COLOR  0xc000

#define GC_BLACK  0x0000
#define GC_WHITE  0x8000
#define GC_GREY   0x4000
#define GC_PURPLE 0xc000

色值的取值,剛好配合了使用高最兩位記錄色值設(shè)計(jì)盏道。

zend_refcounted_h的內(nèi)存分布情況如下圖所示稍浆,共占8字節(jié)。


zend_refcounted_h內(nèi)存分布

源碼中,色值和地址的取設(shè)均采用了巧妙的位運(yùn)算

//php7.0 Zend/zend_gc.h 

/*下面宏中的v為gc.u.v.gc_info*/

//取位置  
/*~GC_COLOR為0011 0000 0000 0000, 剛好將v的高兩位顏色位置0, 取到地址衅枫。*/
#define GC_ADDRESS(v) \
    ((v) & ~GC_COLOR)
    
//設(shè)置位置
#define GC_INFO_SET_ADDRESS(v, a) \
    do {(v) = ((v) & GC_COLOR) | (a);} while (0)
    
//取顏色
#define GC_INFO_GET_COLOR(v) \
    (((zend_uintptr_t)(v)) & GC_COLOR)

//設(shè)置顏色
#define GC_INFO_SET_COLOR(v, c) \
    do {(v) = ((v) & ~GC_COLOR) | (c);} while (0)

1. 為何要進(jìn)行垃圾回收~垃圾的產(chǎn)生

對(duì)于php7中復(fù)雜類型, 當(dāng)變量進(jìn)行賦值嫁艇、傳遞時(shí),會(huì)增加其引用數(shù)(不了解的同學(xué)弦撩,可以參看(《php7引用計(jì)數(shù)》)步咪。unset、return 等釋放變量時(shí)再減掉引用數(shù)益楼,減掉后如果發(fā)現(xiàn)引用計(jì)數(shù)變?yōu)?則直接釋放相應(yīng)內(nèi)存猾漫,這是變量的基本回收過程。

不過有一種情況是這個(gè)機(jī)制無法解決的感凤,那就是循環(huán)引用悯周。

什么是循環(huán)引用呢? 簡單的描述就是變量的內(nèi)部成員引用了變量自身陪竿。這種情況常發(fā)生在數(shù)組和對(duì)象類型的變量上队橙。下面我們看一個(gè)例子。

$a = [1];
$a[] = &$a;

unset($a);

在unset之前萨惑,引用關(guān)系如下圖所示:


unset之前

unset之后引用關(guān)系如下圖所示:


unset之后

當(dāng)執(zhí)行unset操作后捐康,$a所在的zval類型被標(biāo)記為IS_UNDEF,zend_reference結(jié)構(gòu)體的引用計(jì)數(shù)減1庸蔼,但仍然大于0解总,這時(shí),后面的結(jié)構(gòu)就成為了垃圾姐仅,對(duì)此不處理會(huì)造成內(nèi)存泄露花枫。垃圾回收要處理的就是這種情況。

2. 進(jìn)行垃圾回收的條件

  1. 如果一個(gè)變量value的refcount減少之后等于0掏膏,那么此value可以被釋放掉劳翰,不屬于垃圾。GC無需處理馒疹。

  2. 如果一個(gè)變量value的refcount減少之后大于0佳簸,那么此zval還不能被釋放,此zval可能成為一個(gè)垃圾颖变。
    此時(shí)生均,如果zval.u1.type_flag包含IS_TYPE_COLLECTABLE標(biāo)記,則該變量會(huì)被GC收集并進(jìn)行后續(xù)處理腥刹。

//php7.0 Zend/zend_types.h
#define IS_TYPE_COLLECTABLE                      (1<<3)

什么類型的變量會(huì)標(biāo)記為IS_TYPE_COLLECTABLE呢马胧?

|     type       | collectable |
+----------------+-------------+
|simple types    |             |
|string          |             |
|interned string |             |
|array           |      Y      |
|immutable array |             |
|object          |      Y      |
|resource        |             |
|reference       |             |

可見目前垃圾回收只針對(duì)array、object兩種類型衔峰。

這也比較好理解佩脊,數(shù)組的情況上面已經(jīng)介紹了蛙粘,object的情況則是成員屬性引用對(duì)象本身導(dǎo)致的,其它類型不會(huì)出現(xiàn)這種變量中的成員引用變量自身的情況威彰,所以垃圾回收只會(huì)處理這兩種類型的變量出牧。

3. 垃圾回收機(jī)制

垃圾回收過程大致分為兩步:

  1. 將可能是垃圾的變量記錄到垃圾緩存buffer中
  2. 當(dāng)buffer滿后對(duì)每條記錄進(jìn)行檢查,看是否存在循環(huán)引用的情況抱冷,并進(jìn)行回收崔列。

3.1 垃圾緩存~垃圾收集器

3.1.1 zend_gc_globals

zend_gc_globals是垃圾回收過程中主要用到的一個(gè)結(jié)構(gòu)梢褐,用來保存垃圾回收器的所有信息旺遮,比如垃圾緩存區(qū)。zend_gc_globals的數(shù)據(jù)結(jié)構(gòu)如下:

//php7.0 Zend/zend_gc.h
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;

說明:

  • buf: 當(dāng)refcount減少后如果大于0那么就會(huì)將這個(gè)變量的value加入GC的垃圾緩存區(qū)盈咳,buf就是這個(gè)緩存區(qū)耿眉。它實(shí)際是一塊連續(xù)的內(nèi)存,在GC初始化時(shí)一次性分配了GC_ROOT_BUFFER_MAX_ENTRIES數(shù)量個(gè)gc_root_buffer鱼响,插入變量時(shí)直接從buf中取出可用節(jié)點(diǎn)鸣剪。在php7.0源碼中,GC_ROOT_BUFFER_MAX_ENTRIES值為100001丈积。
//php7.0 Zend/zend_gc.c
#define GC_ROOT_BUFFER_MAX_ENTRIES 10001
  • roots: 垃圾緩存鏈表的頭部筐骇,啟動(dòng)GC檢查的過程就是從roots開始遍歷的。
  • 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):
  • last_unused: 與first_unused類似擒悬,指向buf末尾。
  • 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的節(jié)點(diǎn)。

下圖是zend_gc_globals結(jié)構(gòu)的內(nèi)存占用情況摇展,總大小為120字節(jié)吻氧。


zend_gc_globals

PHP7中垃圾回收維護(hù)了一個(gè)全局變量gc_globals,存取值的宏為GC_G(v)。

//php7.0 Zend/zend_gc.c
ZEND_API zend_gc_globals gc_globals;

//php7.0 Zend/zend_gc.h
#define GC_G(v) (gc_globals.v)

3.1.2 gc_root_buffer

gc_root_buffer用來保存每個(gè)可能是垃圾的變量盯孙,它實(shí)際就是整個(gè)垃圾收集buffer鏈表的元素鲁森,當(dāng)GC收集一個(gè)變量時(shí)會(huì)創(chuàng)建一個(gè)gc_root_buffer,插入鏈表振惰。gc_root_buffer組成了一個(gè)雙向鏈表歌溉,其數(shù)據(jù)結(jié)構(gòu)如下:

//php7.0 Zend/zend_gc.h
typedef struct _gc_root_buffer {
    zend_refcounted          *ref;      //每個(gè)zend_value的gc信息
    struct _gc_root_buffer   *next;     /* double-linked list*/
    struct _gc_root_buffer   *prev;
    uint32_t                 refcount;
} gc_root_buffer;

3.1.3 一個(gè)例子

for($i=0; $i<=2; $i++){
    $a[$i] = [$i."_string"];
    $b[] = $a[$i];
    echo "unset $i\n";
    unset($a[$i]);
}

unset(a[i])后,因?yàn)槿匀挥?img class="math-inline" src="https://math.jianshu.com/math?formula=b%E5%AF%B9%E5%BA%94%E7%9A%84%E5%85%83%E7%B4%A0%E6%8C%87%E5%90%91" alt="b對(duì)應(yīng)的元素指向" mathimg="1">a[$i]對(duì)應(yīng)的zend_array骑晶, 所以其引用計(jì)數(shù)不為0痛垛,會(huì)進(jìn)入垃圾回收緩沖區(qū)。相應(yīng)的垃圾收集器的狀態(tài)如下圖所示:

image

來看一下單個(gè)gc_root_buffer中存儲(chǔ)的數(shù)據(jù)桶蛔。我們知道匙头,zend_array和zend_object結(jié)構(gòu)的第一個(gè)字段都是gc,用于記錄引用計(jì)數(shù)等與垃圾回收相關(guān)的數(shù)據(jù)仔雷。當(dāng)一個(gè)變量可能成為垃圾時(shí)蹂析,其實(shí)gc_root_buffer并不是原樣存儲(chǔ)了一份變量相關(guān)的數(shù)據(jù),而是用一個(gè)ref指針指向了變量數(shù)據(jù)對(duì)應(yīng)的gc字段碟婆。

結(jié)合本例电抚,gc_root_buffer.ref就是指向了zend_array.gc,如下圖所示:


gc_root_buffer

3.1.4 源碼解讀

3.1.4.1 gc_init

垃圾回收器初始化

//7.0.14/Zend/zend_gc.c
ZEND_API void gc_init(void)
{
    //buf沒有分配內(nèi)存竖共,且開始了垃圾回收蝙叛,則進(jìn)行內(nèi)存分配和初始化工作
    if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
        //分配buf緩存區(qū)內(nèi)存,大小為GC_ROOT_BUFFER_MAX_ENTRIES(10001)
        GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);
        //last_unused指向緩沖區(qū)末尾
        GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
        
        gc_reset();
    }
}

說明:

  • gc_init在php初始化時(shí)就會(huì)被執(zhí)行肘迎。
  • 可以在php.ini中設(shè)置zend.enable_gc = On甥温,開啟垃圾回收。
  • GC_G是一個(gè)宏妓布,用于獲取全局gc_globals相應(yīng)的字段姻蚓。
  • gc_reset()中主要是將gc_globals的各種字段賦初值,比較重要的代碼如下:
//將first_unused指向buf的第一個(gè)節(jié)點(diǎn)匣沼,空出第0個(gè)位置保留狰挡。
GC_G(first_unused) = GC_G(buf) + 1;
3.1.4.2 gc_init

嘗試將變量加入回收緩沖區(qū)。在unset中就調(diào)用了這個(gè)函數(shù)释涛。

先來看看unset的核心代碼

//php7.0 Zend/zend_vm_execute.h
/*針對(duì)變量不同情況加叁,php定義了很多unset,但其核心代碼是類似的*/
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_UNSET_VAR_SPEC_CV_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS){
            zend_refcounted *garbage = Z_COUNTED_P(var
            //引用計(jì)數(shù)-1, 若為0則直接回收
            if (!--GC_REFCOUNT(garbage)) {
                ZVAL_UNDEF(var);
                zval_dtor_func_for_ptr(garbage);
            } 
            //-1后引用計(jì)數(shù)不為0的情況
            else {
                zval *z = var;
                ZVAL_DEREF(z);
                //變量為collectable類型唇撬,且未加入垃圾回收緩存區(qū)
                if (Z_COLLECTABLE_P(z) && UNEXPECTED(!Z_GC_INFO_P(z))) {
                    ZVAL_UNDEF(var);
                    //嘗試加入緩沖區(qū)
                    gc_possible_root(Z_COUNTED_P(z));
                } else {
                    ZVAL_UNDEF(var);
                }
            }
}

接下來是重頭戲它匕,gc_possible_root。

//php7.0.14/Zend/zend_gc.c

//ref參數(shù)窖认,是zend_value相應(yīng)的gc地址
ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
{
    gc_root_buffer *newRoot;

    if (UNEXPECTED(CG(unclean_shutdown)) || UNEXPECTED(GC_G(gc_active))) {
        return;
    }    

    //檢查類型豫柬,必須是array或object告希,gc中冗余的type在此處發(fā)揮了作用
    ZEND_ASSERT(GC_TYPE(ref) == IS_ARRAY || GC_TYPE(ref) == IS_OBJECT);
    //檢查必須是黑色,說明沒有加入過緩沖區(qū)烧给。關(guān)于染色機(jī)制燕偶,在3.2節(jié)中會(huì)詳細(xì)講述。
    ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK));
    ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref)));

    //首先嘗試在unused隊(duì)列中取一個(gè)buffer
    newRoot = GC_G(unused);
    if (newRoot) {
        //從unused隊(duì)列中取到一個(gè)buffer, unused后移
        GC_G(unused) = newRoot->prev;
    } 
    //buffer隊(duì)列未滿础嫡,則從first_unused取一個(gè)buffer, 同時(shí)將first_unused后移指么。
    else if (GC_G(first_unused) != GC_G(last_unused)) {
        newRoot = GC_G(first_unused);
        GC_G(first_unused)++;
    } 
    //緩沖區(qū)已滿的情況
    else {
        //未開啟gc,返回
        if (!GC_G(gc_enabled)) {
            return;
        }    
        
        //此處為具體的垃圾加收算法榴鼎,將在3.2節(jié)中講述伯诬。
        GC_REFCOUNT(ref)++;
        gc_collect_cycles();
        GC_REFCOUNT(ref)--;
        
        //變量的引用計(jì)數(shù)為0, 直接銷毀
        if (UNEXPECTED(GC_REFCOUNT(ref)) == 0) { 
            zval_dtor_func_for_ptr(ref);
            return;
        }    
        
        //gc.u.v.gc_info有值,說明已加入過buffer檬贰。
        if (UNEXPECTED(GC_INFO(ref))) {
            return;
        }
        
        //垃圾加收后(如果有成功收回的姑廉,則回收的buffer會(huì)加入unused隊(duì)列)缺亮,嘗試從unused取buffer
        newRoot = GC_G(unused);
        //依然沒有buffer空間翁涤,返回
        if (!newRoot) {
            return;
        }

        GC_G(unused) = newRoot->prev;
    }
    
    //gc.u.v.gc_info中記錄buf位置和顏色。將變量gc染為紫色萌踱,表明變量已進(jìn)入緩存區(qū)葵礼,染色機(jī)制將在3.2節(jié)詳細(xì)講述。
    GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
    
    //新的gc_root_buffer.ref指向變量的gc(3.1.3例子的圖示)
    newRoot->ref = ref;

    //調(diào)整指針并鸵,使得gc_root_buffer加入雙向隊(duì)列
    newRoot->next = GC_G(roots).next;
    newRoot->prev = &GC_G(roots);
    GC_G(roots).next->prev = newRoot;
    GC_G(roots).next = newRoot;
}

3.1.5 gdb 深入查看roots鏈上的數(shù)據(jù)鸳粉。

對(duì)于3.1.3中的例子,使用gdb园担,詳細(xì)看下掛接在roots鏈上的數(shù)據(jù)届谈。
在命令行下執(zhí)行g(shù)db php, 進(jìn)入gdb調(diào)試
首先設(shè)置斷點(diǎn)弯汰。

(gdb) b /usr/local/src/php-7.0.14/Zend/zend_gc.c:271
Breakpoint 2 at 0x6664c2: file /usr/local/src/php-7.0.14/Zend/zend_gc.c, line 271.
  • zend_gc.c:271 剛好是給gc_possible_root()函數(shù)給newRoot賦完值的位置艰山,停在這里方便我們觀察數(shù)據(jù)。

下面開始調(diào)試

(gdb) run ref.php
Starting program: /search/php70/bin/php ref.php

Breakpoint 1, gc_possible_root (ref=<value optimized out>) at /usr/local/src/php-7.0.14/Zend/zend_gc.c:271
271             GC_G(roots).next->prev = newRoot;
/*注意咏闪,php可能會(huì)有一些自己的變量加入到roots環(huán)曙搬。
 *這時(shí)我們需要使用c命令繼續(xù)執(zhí)行,直到看到unset ...的輸出鸽嫂,表明這時(shí)是我們自己代碼的變量進(jìn)入了gc_possible_root纵装。
 這也是我們?cè)诖a里加入echo的用途所在。*/
(gdb) c
Continuing.
unset 0

/*有了unset 0据某,此時(shí)是我們的變量進(jìn)入gc_possible_root了*/
Breakpoint 1, gc_possible_root (ref=<value optimized out>) at /usr/local/src/php-7.0.14/Zend/zend_gc.c:271
271             GC_G(roots).next->prev = newRoot;

下面來看newRoot的具體信息

//newRoot就是roots鏈上的元素
(gdb) p newRoot
$2 = (gc_root_buffer *) 0x7ffff7ae9050
//看下元素的內(nèi)容
(gdb) p *newRoot
//ref應(yīng)該指向$a[0]頭上的gc字段
$3 = {ref = 0x7ffff7856230, next = 0x7ffff7ae9030, prev = 0xb13df0, refcount = 0}

(gdb) p *newRoot.ref
/* 引用計(jì)數(shù)為1橡娄,
 * type為7,表明類型是數(shù)組癣籽,符合我們的預(yù)期挽唉。
 * gc_info為49154, 對(duì)應(yīng)二進(jìn)制1100 0000 0000 0010扳还,紫色,在buf上的第2個(gè)位置
 */
$4 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 49154}, type_info = 3221356551}}}

代碼中橱夭,a有兩個(gè)元素氨距,來看第二個(gè)元素a[1]進(jìn)入gc_possible_root的情況。

(gdb) c
Continuing.
unset 1

Breakpoint 1, gc_possible_root (ref=<value optimized out>) at /usr/local/src/php-7.0.14/Zend/zend_gc.c:271
271             GC_G(roots).next->prev = newRoot;

(gdb) p *newRoot.ref
/* gc_info為49155, 對(duì)應(yīng)二進(jìn)制1100 0000 0000 0011棘劣,紫色俏让,在buf上的第3個(gè)位置。
 * 上一步$a[0]在第2個(gè)位置茬暇,兩者剛好相鄰首昔。
 */
$7 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 49155}, type_info = 3221422087}}}

看下這個(gè)元素的具體內(nèi)容,以確認(rèn)它真的是我們的$a[1]

(gdb) p *(zend_array*)newRoot.ref
/* zend_array的詳情
 * arData指向真實(shí)數(shù)據(jù)
 * nNumUsed=1:存了一個(gè)元素
 */
$13 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 49155}, type_info = 3221422087}}, u = {v = {
      flags = 30 '\036', nApplyCount = 0 '\000', nIteratorsCount = 0 '\000', reserve = 0 '\000'}, flags = 30}, 
  nTableMask = 4294967294, arData = 0x7ffff785cb48, nNumUsed = 1, nNumOfElements = 1, nTableSize = 8, nInternalPointer = 0, 
  nNextFreeElement = 1, pDestructor = 0x62faa0 <_zval_ptr_dtor>}
 

(gdb) p ((zend_array*)newRoot.ref).arData[0]
/*查看該元素內(nèi)容糙俗,type為6勒奇,字串類型,符合預(yù)期*/
$14 = {val = {value = {lval = 140737346147544, dval = 6.9533487818369398e-310, counted = 0x7ffff78614d8, 
      str = 0x7ffff78614d8, arr = 0x7ffff78614d8, obj = 0x7ffff78614d8, res = 0x7ffff78614d8, ref = 0x7ffff78614d8, 
      ast = 0x7ffff78614d8, zv = 0x7ffff78614d8, ptr = 0x7ffff78614d8, ce = 0x7ffff78614d8, func = 0x7ffff78614d8, ww = {
        w1 = 4152759512, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 20 '\024', const_flags = 0 '\000', 
        reserved = 0 '\000'}, type_info = 5126}, u2 = {var_flags = 0, next = 0, cache_slot = 0, lineno = 0, num_args = 0, 
      fe_pos = 0, fe_iter_idx = 0}}, h = 0, key = 0x0}

/*查看字串的存儲(chǔ) 
 *  val為1巧骚,表明字串第一字符為1
 *  len為8赊颠,表明字串長度為8
 */
(gdb) p *((zend_array*)newRoot.ref).arData[0].val.value.str
$15 = {gc = {refcount = 1, u = {v = {type = 6 '\006', flags = 0 '\000', gc_info = 0}, type_info = 6}}, h = 0, len = 8, 
  val = "1"}
  
//打印字串具體內(nèi)容,的確是$a[1]存儲(chǔ)的字串
(gdb) p *((zend_array*)newRoot.ref).arData[0].val.value.str.val@8
$18 = "1_string"

3.2 垃圾回收算法

3.2.1 算法描述

  1. 遍歷roots鏈表劈彪, 把當(dāng)前元素標(biāo)為灰色(zend_refcounted_h.gc_info置為GC_GREY)竣蹦,然后對(duì)當(dāng)前元素的成員進(jìn)行深度優(yōu)先遍歷,把成員的refcount減1沧奴,并且也標(biāo)為灰色痘括。(gc_mark_roots())
  2. 遍歷roots鏈表中所有灰色元素及其子元素,如果發(fā)現(xiàn)其引用計(jì)數(shù)仍舊大于0滔吠,說明這個(gè)元素還在其他地方使用纲菌,那么將其顏色重新標(biāo)記會(huì)黑色,并將其引用計(jì)數(shù)加1(在第一步有減1操作)疮绷。如果發(fā)現(xiàn)其引用計(jì)數(shù)為0翰舌,則將其標(biāo)記為白色。(gc_scan_roots())
  3. 遍歷roots鏈表矗愧,將黑色的元素從roots移除灶芝。然后對(duì)roots中顏色為白色的元素進(jìn)行深度優(yōu)先遍歷,將其引用計(jì)數(shù)加1(在第一步有減1操作)唉韭,同時(shí)將顏色為白色的子元素也加入roots鏈表夜涕。最后然后將roots鏈表移動(dòng)到待釋放的列表to_free中。(gc_collect_roots())
  4. 釋放to_free列表的元素属愤。

3.2.2 元素顏色轉(zhuǎn)化圖

元素顏色轉(zhuǎn)化

3.2.3 為什么算法是有效的

為什么算法可以找到垃圾呢女器?我們知道,垃圾產(chǎn)生的原因就是循環(huán)引用住诸,也就是說子元素指向了元素自身驾胆,使用引用計(jì)數(shù)無法清0涣澡。

算法的核心就是嘗試遍歷子元素,將其引用計(jì)數(shù)減1丧诺,若有循環(huán)引用的情況入桂,則在減子元素引用計(jì)數(shù)后,必可使原始元素的引用計(jì)數(shù)清0驳阎。

再來回憶下第一節(jié)這樣循環(huán)引用的圖抗愁。


循環(huán)引用

因?yàn)閦end_array的子元素引用了自身,導(dǎo)致垃圾呵晚。

我們看看算法是如何清除垃圾的:

  1. 遍歷zend_array蜘腌,對(duì)arData中每個(gè)元素,將其引用計(jì)數(shù)-1饵隙。
  2. 遍歷到第1個(gè)元素時(shí)撮珠,發(fā)現(xiàn)其指向引用類型。源碼實(shí)現(xiàn)中有這樣一段:
/php-7.0.14/Zend/zend_gc.c
/*如果是引用類型金矛,則將它內(nèi)部的zval對(duì)應(yīng)的數(shù)據(jù)的引用計(jì)數(shù)減1*/
else if (GC_TYPE(ref) == IS_REFERENCE) {             
    if (Z_REFCOUNTED(((zend_reference*)ref)->val)) {   
        .... 
        ref = Z_COUNTED(((zend_reference*)ref)->val);  
        GC_REFCOUNT(ref)--;                            
        ...
    }
}

對(duì)應(yīng)到我們的例子芯急,就是將zend_array的引用計(jì)數(shù)減1,這時(shí)zend_array的引用計(jì)數(shù)就為0了绷柒,可以回收了志于!

3.2.4 核心代碼解讀

gc_possible_root()中調(diào)用了gc_collect_cycles()來進(jìn)行垃圾回收涮因。gc_collect_cycles是一個(gè)函數(shù)指針废睦, 定義如下:

//php7.0 Zend/zend_gc.c
ZEND_API int (*gc_collect_cycles)(void);

在php-7.0.14/UPGRADING.INTERNALS中有一段說明

gc_collect_cycles() is now a function pointer, and can be replaced in the same manner as zend_execute_ex() if needed (for example, to include the time spent in the garbage collector in a profiler). The default implementation has been renamed to zend_gc_collect_cycles(), and is exported with ZEND_API.

可見zend_gc_collect_cycles默認(rèn)實(shí)現(xiàn)是zend_gc_collect_cycles()的。下面我們就來看下zend_gc_collect_cycles的代碼养泡。

//php7.0 Zend/zend_gc.c

ZEND_API int zend_gc_collect_cycles(void)
{
    int count = 0;
    /*
     *緩存沖區(qū)初始化時(shí)(gc_reset())設(shè)置了 GC_G(roots).next = &GC_G(roots), 
     *所以只有GC_G(roots).next != &GC_G(roots)才說明roots鏈不空
     */
    if (GC_G(roots).next != &GC_G(roots)) {
        
        gc_root_buffer *current, *next, *orig_next_to_free;
        zend_refcounted *p;
        gc_root_buffer to_free;
        uint32_t gc_flags = 0;
        gc_additional_buffer *additional_buffer;
        
        
        /*如果已有回收活動(dòng)正在進(jìn)行嗜湃,則返回*/
        if (GC_G(gc_active)) {
            return 0;
        }

        GC_TRACE("Collecting cycles");
        GC_G(gc_runs)++;
        GC_G(gc_active) = 1;

        GC_TRACE("Marking roots");
        //遍歷roots鏈表,對(duì)當(dāng)前節(jié)點(diǎn)value的所有成員(如數(shù)組元素澜掩、成員屬性)進(jìn)行深度優(yōu)先遍歷把成員refcount減1
        gc_mark_roots();
        
        GC_TRACE("Scanning roots");
        /*遍歷roots鏈表中所有灰色元素及其子元素购披,如果發(fā)現(xiàn)其引用計(jì)數(shù)仍舊大于0,說明這個(gè)元素還在其他地方使用肩榕,那么將其顏色重新標(biāo)記會(huì)黑色刚陡,并將其引用計(jì)數(shù)加1。如果發(fā)現(xiàn)其引用計(jì)數(shù)為0株汉,則將其標(biāo)記為白色筐乳。*/
        gc_scan_roots();

        GC_TRACE("Collecting roots");
        additional_buffer = NULL;
        /*遍歷roots鏈表,將黑色的元素從roots移除乔妈。
         對(duì)roots中顏色為白色的元素進(jìn)行深度優(yōu)先遍歷蝙云,將其引用計(jì)數(shù)加1,同時(shí)將顏色為白色的子元素也加入roots鏈表路召。
         最后然后將roots鏈表移動(dòng)到待釋放的列表to_free中勃刨。
         
         關(guān)于additional_buffer波材,在3.2.5節(jié)中做詳細(xì)說明
         */
        count = gc_collect_roots(&gc_flags, &additional_buffer);

        GC_G(gc_active) = 0;
    
        if (GC_G(to_free).next == &GC_G(to_free)) {
            /* nothing to free */
            GC_TRACE("Nothing to free");
            return 0;
        }

        /* Copy global to_free list into local list */
        to_free.next = GC_G(to_free).next;
        to_free.prev = GC_G(to_free).prev;
        to_free.next->prev = &to_free;
        to_free.prev->next = &to_free;

        /* Free global list */
        GC_G(to_free).next = &GC_G(to_free);
        GC_G(to_free).prev = &GC_G(to_free);

        orig_next_to_free = GC_G(next_to_free);

        ... ...

        /*釋放to_free上的垃圾*/
        GC_TRACE("Destroying zvals");
        GC_G(gc_active) = 1;
        current = to_free.next;
        while (current != &to_free) {
            p = current->ref;
            GC_G(next_to_free) = current->next;
            GC_TRACE_REF(p, "destroying");
            
            //釋放object
            if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_OBJECT) {
                zend_object *obj = (zend_object*)p;
                ...
                //調(diào)用free_obj釋放對(duì)象
                if (obj->handlers->free_obj) {
                    GC_REFCOUNT(obj)++;
                    obj->handlers->free_obj(obj);
                    GC_REFCOUNT(obj)--;
                }
                ...
            } 
            //釋放數(shù)組
            else if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_ARRAY) {
                zend_array *arr = (zend_array*)p;

                GC_TYPE(arr) = IS_NULL;
                zend_hash_destroy(arr);
            }
            current = GC_G(next_to_free);
        }

        /*回收使用過的垃圾池buffer,將其放入unused隊(duì)列*/
        current = to_free.next;
        while (current != &to_free) {
            next = current->next;
            p = current->ref;
            /*
             *只有在原有垃圾緩存區(qū)的buffer才可以加入unused
             *之所有有此判斷身隐,與additional_buffer相關(guān)
             */
            if (EXPECTED(current >= GC_G(buf) && current < GC_G(buf) + GC_ROOT_BUFFER_MAX_ENTRIES)) {
                current->prev = GC_G(unused);
                GC_G(unused) = current;
            }
            efree(p);
            current = next;
        }
    
        //回收additional_buffer的內(nèi)存
        while (additional_buffer != NULL) {
            gc_additional_buffer *next = additional_buffer->next;
            efree(additional_buffer);
            additional_buffer = next;
        }

        GC_TRACE("Collection finished");
        GC_G(collected) += count;
        GC_G(next_to_free) = orig_next_to_free;

        GC_G(gc_active) = 0;
    }

    return count;
}

3.2.5 gc_additional_buffer

3.2.5.1 gc_additional_buffer的用途

在執(zhí)行g(shù)c_collect_roots()時(shí)廷区,用到了gc_additional_buffer, 這個(gè)結(jié)構(gòu)的用途是什么呢?
通過上面的說明我們知道贾铝,roots上存儲(chǔ)了所有可能是垃圾的元素躲因,但是并沒有存放這些元素的子元素。在
在執(zhí)行g(shù)c_collect_roots()時(shí)忌傻,我們做的很重要的一件事就是將所有白色元素放到roots鏈上大脉,這當(dāng)然也包括白色的子元素。子元素可能有很多水孩,但受限于垃圾緩沖池的大小roots最長只有10000個(gè)镰矿,不夠用怎么辦呢?這時(shí)就需要臨時(shí)申請(qǐng)額外的存儲(chǔ)空間gc_additional_buffer俘种。

3.2.5.2 gc_additional_buffer的結(jié)構(gòu)

gc_additional_buffer結(jié)構(gòu)如下

//Zend/zend_gc.c
typedef struct _gc_addtional_bufer gc_additional_buffer;

struct _gc_addtional_bufer {
    uint32_t              used;
    gc_additional_buffer *next;
    gc_root_buffer        buf[GC_NUM_ADDITIONAL_ENTRIES];
};

每個(gè)gc_additional_buffer中有GC_NUM_ADDITIONAL_ENTRIES個(gè)gc_root_buffer秤标,可用于存儲(chǔ)待回收的垃圾。當(dāng)一個(gè)gc_additional_buffer不夠用時(shí)宙刘,就會(huì)再申請(qǐng)一個(gè)gc_additional_buffer, 多個(gè)gc_additional_buffer使用next指針串連苍姜,形成鏈表。

gc_additional_buffer
3.2.5.3 gc_additional_buffer的具體使用

gc_additional_buffer在gc_add_garbage()中使用悬包,gc_add_garbage的功能是將不在roots鏈上的白色元素掛接到roots鏈上衙猪。
調(diào)用棧如下:

gc_add_garbage()
gc_collect_white()
gc_collect_roots()

下面來具體看下gc_add_garbage的實(shí)現(xiàn)

static void gc_add_garbage(zend_refcounted *ref, gc_additional_buffer **additional_buffer){
    //首先嘗試從unused鏈上取buffer
    gc_root_buffer *buf = GC_G(unused);    
    if (buf) {        
        GC_G(unused) = buf->prev;    
        /* optimization: color is already GC_BLACK (0) */
        //記錄buf在緩沖池中的位置
        GC_INFO(ref) = buf - GC_G(buf);
    }
    //接下來嘗試從first_unused取一個(gè)buffer
    else if (GC_G(first_unused) != GC_G(last_unused)) {        
        buf = GC_G(first_unused);        
        GC_G(first_unused)++;
        //記錄buf在緩沖池中的位置
        GC_INFO(ref) = buf - GC_G(buf);
    }
    //現(xiàn)有垃圾回收池滿了
    else {        
     /* If we don't have free slots in the buffer, allocate a new one and         
      * set it's address to GC_ROOT_BUFFER_MAX_ENTRIES that have special meaning.
      */
        //沒有additional_buffer或者當(dāng)前additional_buffer已裝滿
        if (!*additional_buffer || (*additional_buffer)->used == GC_NUM_ADDITIONAL_ENTRIES) {
            //新申請(qǐng)內(nèi)存裝初始化一個(gè)additional_buffer
            gc_additional_buffer *new_buffer = emalloc(sizeof(gc_additional_buffer));
            new_buffer->used = 0;
            new_buffer->next = *additional_buffer;
            *additional_buffer = new_buffer;
        }
        //從當(dāng)前additional_buffe上取一個(gè)buffer
        buf = (*additional_buffer)->buf + (*additional_buffer)->used;
        (*additional_buffer)->used++;
        
        /*
         * 將buf位置記錄為GC_ROOT_BUFFER_MAX_ENTRIES
         * 注意GC_ROOT_BUFFER_MAX_ENTRIES是不存在于原有垃圾緩沖區(qū)的一個(gè)位置
         */
        GC_INFO(ref) = GC_ROOT_BUFFER_MAX_ENTRIES;
        
        /* modify type to prevent indirect destruction */
        GC_TYPE(ref) |= GC_FAKE_BUFFER_FLAG;
    }
    
    //取到buffer, 記錄信息并將其掛接到roots鏈
    if (buf) {
        GC_REFCOUNT(ref)++;
        buf->ref = ref;
        buf->next = GC_G(roots).next;
        buf->prev = &GC_G(roots);
        GC_G(roots).next->prev = buf;
        GC_G(roots).next = buf;
    }
}

4. 再說gc結(jié)構(gòu)

為什么gc要放在復(fù)雜變量的頭部?為什么zval中有變量類型布近,gc中要再記錄一份垫释?

回憶一下php7中變量的存儲(chǔ)方式
一個(gè)zval中包含一個(gè)zend_value結(jié)構(gòu)撑瞧,zend_value中相應(yīng)類型的指針指向?qū)?yīng)類型的實(shí)際存儲(chǔ)空間棵譬。

一個(gè)array類形的變量,存儲(chǔ)方式如下圖所示预伺。


array

zend_value中的arr指向了zend_array订咸。

在垃圾處理過程中,我們主要用到的都是指向gc的指針酬诀。但是在染色時(shí)脏嚷,我們需根據(jù)變量類型對(duì)變量內(nèi)部存儲(chǔ)的子元素。這時(shí)怎么辦呢料滥?看垃圾加收過程中的代碼:

//php7.0 Zend/zend_gc.c
static void gc_mark_grey(zend_refcounted *ref)
{
    HashTable *ht;
    Bucket *p, *end;
    zval *zv;
    
    if (GC_REF_GET_COLOR(ref) != GC_GREY) {
        //染成灰色
        GC_REF_SET_COLOR(ref, GC_GREY);
            if (GC_TYPE(ref) == IS_OBJECT) {
                zend_object_get_gc_t get_gc;
                //轉(zhuǎn)換為zend_object類型
                zend_object *obj = (zend_object*)ref;
        
                ... ...
            }
            else if (GC_TYPE(ref) == IS_ARRAY) {
                ...
                //轉(zhuǎn)換為zend_array類型
                ht = (zend_array*)ref;
                ...
            }
        }
    }
    ... ...
}

這段代碼的功能是將元素及其子元素染成灰色然眼,由gc_mark_roots()調(diào)用。它接收的參數(shù)ref變是一個(gè)指向gc的指針葵腹。

GC_TYPE(ref)用于獲取變量類型高每。這也是為什么zval中記錄了變量類型屿岂,我們?nèi)匀灰趃c中冗余一份的原因。

#define GC_TYPE(p)                  (p)->gc.u.v.type

因?yàn)間c在相應(yīng)數(shù)據(jù)類型的起始位置鲸匿,所以爷怀,在知道具體類型后,我們只要使用強(qiáng)制類型轉(zhuǎn)換带欢,就可以將指向gc類型的指向轉(zhuǎn)為指向具體類型的指針运授,并通過類型指針取到變量具體數(shù)據(jù)。

zend_object *obj = (zend_object*)ref;
ht = (zend_array*)ref;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末乔煞,一起剝皮案震驚了整個(gè)濱河市吁朦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渡贾,老刑警劉巖逗宜,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異空骚,居然都是意外死亡纺讲,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門囤屹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熬甚,“玉大人,你說我怎么就攤上這事肋坚∠缋ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵冲簿,是天一觀的道長粟判。 經(jīng)常有香客問我,道長峦剔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任角钩,我火速辦了婚禮吝沫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘递礼。我一直安慰自己惨险,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開白布脊髓。 她就那樣靜靜地躺著辫愉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪将硝。 梳的紋絲不亂的頭發(fā)上恭朗,一...
    開封第一講書人閱讀 52,549評(píng)論 1 312
  • 那天屏镊,我揣著相機(jī)與錄音,去河邊找鬼痰腮。 笑死而芥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的膀值。 我是一名探鬼主播棍丐,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼沧踏!你這毒婦竟也來了歌逢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤翘狱,失蹤者是張志新(化名)和其女友劉穎趋翻,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盒蟆,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡踏烙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了历等。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片讨惩。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖寒屯,靈堂內(nèi)的尸體忽然破棺而出荐捻,到底是詐尸還是另有隱情,我是刑警寧澤寡夹,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布处面,位于F島的核電站,受9級(jí)特大地震影響菩掏,放射性物質(zhì)發(fā)生泄漏魂角。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一智绸、第九天 我趴在偏房一處隱蔽的房頂上張望野揪。 院中可真熱鬧,春花似錦瞧栗、人聲如沸斯稳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挣惰。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間憎茂,已是汗流浹背珍语。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唇辨,地道東北人廊酣。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像赏枚,于是被迫代替她去往敵國和親亡驰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361