php7垃圾回收机制及相关源码解读

0. gc的基本结构

《php7的引用计数》一文中,我们说过,php7的复杂类型,像字符串、数组、引用等的数据结构中,头部都有一个gc,变量的引用计数维护在这个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字节。type_info中的4字节(每个字节8bit)有着各自的意义,分别如下:

  • type:当前元素的类型,同zval的u1.v.type。(为何要冗余记录一份,我们在第4部分讲解。)
  • flags:标记数据类型,可以是字符串类型或数组类型等。其中标记字符串的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 */

标记数组的flags有:

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

标记对象的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:后面的两个字节标记当前元素的颜色和垃圾回收池中的位置,其中高地址的两位用来标记颜色,低地址的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

色值的取值,刚好配合了使用高最两位记录色值设计。

zend_refcounted_h的内存分布情况如下图所示,共占8字节。
image

源码中,色值和地址的取设均采用了巧妙的位运算

//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)
    
//设置位置
#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)

//设置颜色
#define GC_INFO_SET_COLOR(v, c) \
    do {(v) = ((v) & ~GC_COLOR) | (c);} while (0)

1. 为何要进行垃圾回收~垃圾的产生

对于php7中复杂类型, 当变量进行赋值、传递时,会增加其引用数(不了解的同学,可以参看(《php7引用计数》)。unset、return 等释放变量时再减掉引用数,减掉后如果发现引用计数变为0则直接释放相应内存,这是变量的基本回收过程。

不过有一种情况是这个机制无法解决的,那就是循环引用。

什么是循环引用呢? 简单的描述就是变量的内部成员引用了变量自身。这种情况常发生在数组和对象类型的变量上。下面我们看一个例子。

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

unset($a);

在unset之前,引用关系如下图所示:
image

unset之后引用关系如下图所示:
image

当执行unset操作后,$a所在的zval类型被标记为IS_UNDEF,zend_reference结构体的引用计数减1,但仍然大于0,这时,后面的结构就成为了垃圾,对此不处理会造成内存泄露。垃圾回收要处理的就是这种情况。

2. 进行垃圾回收的条件

  1. 如果一个变量value的refcount减少之后等于0,那么此value可以被释放掉,不属于垃圾。GC无需处理。

  2. 如果一个变量value的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾。
    此时,如果zval.u1.type_flag包含IS_TYPE_COLLECTABLE标记,则该变量会被GC收集并进行后续处理。

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

什么类型的变量会标记为IS_TYPE_COLLECTABLE呢?

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

可见目前垃圾回收只针对array、object两种类型。

这也比较好理解,数组的情况上面已经介绍了,object的情况则是成员属性引用对象本身导致的,其它类型不会出现这种变量中的成员引用变量自身的情况,所以垃圾回收只会处理这两种类型的变量。

3. 垃圾回收机制

垃圾回收过程大致分为两步:

  1. 将可能是垃圾的变量记录到垃圾缓存buffer中
  2. 当buffer满后对每条记录进行检查,看是否存在循环引用的情况,并进行回收。

3.1 垃圾缓存~垃圾收集器

3.1.1 zend_gc_globals

zend_gc_globals是垃圾回收过程中主要用到的一个结构,用来保存垃圾回收器的所有信息,比如垃圾缓存区。zend_gc_globals的数据结构如下:

//php7.0 Zend/zend_gc.h
typedef struct _zend_gc_globals {
    zend_bool         gc_enabled;   //是否启用gc
    zend_bool         gc_active;    //是否在垃圾检查过程中
    zend_bool         gc_full;      //缓存区是否已满

    gc_root_buffer   *buf;              //启动时分配的用于保存可能垃圾的缓存区
    gc_root_buffer    roots;            //指向buf中最新加入的一个可能垃圾
    gc_root_buffer   *unused;           //指向buf中没有使用的buffer
    gc_root_buffer   *first_unused;     //指向buf中第一个没有使用的buffer
    gc_root_buffer   *last_unused;      //指向buf尾部

    gc_root_buffer    to_free;          //待释放的垃圾列表
    gc_root_buffer   *next_to_free;     //下一待释放的垃圾列表  

    uint32_t gc_runs;       //统计gc运行次数
    uint32_t collected;     //统计已回收的垃圾数
} zend_gc_globals;

说明:

  • buf: 当refcount减少后如果大于0那么就会将这个变量的value加入GC的垃圾缓存区,buf就是这个缓存区。它实际是一块连续的内存,在GC初始化时一次性分配了GC_ROOT_BUFFER_MAX_ENTRIES数量个gc_root_buffer,插入变量时直接从buf中取出可用节点。在php7.0源码中,GC_ROOT_BUFFER_MAX_ENTRIES值为100001。
//php7.0 Zend/zend_gc.c
#define GC_ROOT_BUFFER_MAX_ENTRIES 10001
  • roots: 垃圾缓存链表的头部,启动GC检查的过程就是从roots开始遍历的。
  • first_unused: 指向buf中第一个可用的节点,初始化时这个值为1而不是0,因为第一个gc_root_buffer保留没有使用,有元素插入roots时如果first_unused还没有到达buf的尾部则返回first_unused给最新的元素,然后first_unused++,直到last_unused,比如现在已经加入了2个可能的垃圾变量,则对应的结构:
  • last_unused: 与first_unused类似,指向buf末尾。
  • unused: 有些变量加入垃圾缓存区之后其refcount又减为0了,这种情况就需要从roots中删掉,因为它不可能是垃圾,这样就导致roots链表并不是像buf分配的那样是连续的,中间会出现一些开始加入后面又删除的节点,这些节点就通过unused串成一个单链表,unused指向链表尾部,下次有新的变量插入roots时优先使用unused的这些节点,其次才是first_unused的节点。

下图是zend_gc_globals结构的内存占用情况,总大小为120字节。
image

PHP7中垃圾回收维护了一个全局变量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用来保存每个可能是垃圾的变量,它实际就是整个垃圾收集buffer链表的元素,当GC收集一个变量时会创建一个gc_root_buffer,插入链表。gc_root_buffer组成了一个双向链表,其数据结构如下:

//php7.0 Zend/zend_gc.h
typedef struct _gc_root_buffer {
    zend_refcounted          *ref;      //每个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 一个例子
for($i=0; $i<=2; $i++){
    $a[$i] = [$i."_string"];
    $b[] = $a[$i];
    echo "unset $i\n";
    unset($a[$i]);
}

unset( a [ a[ a[i])后,因为仍然有 b 对 应 的 元 素 指 向 b对应的元素指向 ba[$i]对应的zend_array, 所以其引用计数不为0,会进入垃圾回收缓冲区。相应的垃圾收集器的状态如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sJVgLju5-1600080325296)(https://raw.githubusercontent.com/pangudashu/php7-internal/master/img/zend_gc_1.png)]

来看一下单个gc_root_buffer中存储的数据。我们知道,zend_array和zend_object结构的第一个字段都是gc,用于记录引用计数等与垃圾回收相关的数据。当一个变量可能成为垃圾时,其实gc_root_buffer并不是原样存储了一份变量相关的数据,而是用一个ref指针指向了变量数据对应的gc字段

结合本例,gc_root_buffer.ref就是指向了zend_array.gc,如下图所示:
image

3.1.4 源码解读
3.1.4.1 gc_init

垃圾回收器初始化

//7.0.14/Zend/zend_gc.c
ZEND_API void gc_init(void)
{
    //buf没有分配内存,且开始了垃圾回收,则进行内存分配和初始化工作
    if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
        //分配buf缓存区内存,大小为GC_ROOT_BUFFER_MAX_ENTRIES(10001)
        GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);
        //last_unused指向缓冲区末尾
        GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
        
        gc_reset();
    }
}

说明:

  • gc_init在php初始化时就会被执行。
  • 可以在php.ini中设置zend.enable_gc = On,开启垃圾回收。
  • GC_G是一个宏,用于获取全局gc_globals相应的字段。
  • gc_reset()中主要是将gc_globals的各种字段赋初值,比较重要的代码如下:
//将first_unused指向buf的第一个节点,空出第0个位置保留。
GC_G(first_unused) = GC_G(buf) + 1;
3.1.4.2 gc_init

尝试将变量加入回收缓冲区。在unset中就调用了这个函数。

先来看看unset的核心代码

//php7.0 Zend/zend_vm_execute.h
/*针对变量不同情况,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
            //引用计数-1, 若为0则直接回收
            if (!--GC_REFCOUNT(garbage)) {
                ZVAL_UNDEF(var);
                zval_dtor_func_for_ptr(garbage);
            } 
            //-1后引用计数不为0的情况
            else {
                zval *z = var;
                ZVAL_DEREF(z);
                //变量为collectable类型,且未加入垃圾回收缓存区
                if (Z_COLLECTABLE_P(z) && UNEXPECTED(!Z_GC_INFO_P(z))) {
                    ZVAL_UNDEF(var);
                    //尝试加入缓冲区
                    gc_possible_root(Z_COUNTED_P(z));
                } else {
                    ZVAL_UNDEF(var);
                }
            }
}

接下来是重头戏,gc_possible_root。

//php7.0.14/Zend/zend_gc.c

//ref参数,是zend_value相应的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在此处发挥了作用
    ZEND_ASSERT(GC_TYPE(ref) == IS_ARRAY || GC_TYPE(ref) == IS_OBJECT);
    //检查必须是黑色,说明没有加入过缓冲区。关于染色机制,在3.2节中会详细讲述。
    ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK));
    ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref)));

    //首先尝试在unused队列中取一个buffer
    newRoot = GC_G(unused);
    if (newRoot) {
        //从unused队列中取到一个buffer, unused后移
        GC_G(unused) = newRoot->prev;
    } 
    //buffer队列未满,则从first_unused取一个buffer, 同时将first_unused后移。
    else if (GC_G(first_unused) != GC_G(last_unused)) {
        newRoot = GC_G(first_unused);
        GC_G(first_unused)++;
    } 
    //缓冲区已满的情况
    else {
        //未开启gc,返回
        if (!GC_G(gc_enabled)) {
            return;
        }    
        
        //此处为具体的垃圾加收算法,将在3.2节中讲述。
        GC_REFCOUNT(ref)++;
        gc_collect_cycles();
        GC_REFCOUNT(ref)--;
        
        //变量的引用计数为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会加入unused队列),尝试从unused取buffer
        newRoot = GC_G(unused);
        //依然没有buffer空间,返回
        if (!newRoot) {
            return;
        }

        GC_G(unused) = newRoot->prev;
    }
    
    //gc.u.v.gc_info中记录buf位置和颜色。将变量gc染为紫色,表明变量已进入缓存区,染色机制将在3.2节详细讲述。
    GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
    
    //新的gc_root_buffer.ref指向变量的gc(3.1.3例子的图示)
    newRoot->ref = ref;

    //调整指针,使得gc_root_buffer加入双向队列
    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链上的数据。

对于3.1.3中的例子,使用gdb,详细看下挂接在roots链上的数据。
在命令行下执行gdb php, 进入gdb调试
首先设置断点。

(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()函数给newRoot赋完值的位置,停在这里方便我们观察数据。

下面开始调试

(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可能会有一些自己的变量加入到roots环。
 *这时我们需要使用c命令继续执行,直到看到unset ...的输出,表明这时是我们自己代码的变量进入了gc_possible_root。
 这也是我们在代码里加入echo的用途所在。*/
(gdb) c
Continuing.
unset 0

/*有了unset 0,此时是我们的变量进入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
//看下元素的内容
(gdb) p *newRoot
//ref应该指向$a[0]头上的gc字段
$3 = {ref = 0x7ffff7856230, next = 0x7ffff7ae9030, prev = 0xb13df0, refcount = 0}

(gdb) p *newRoot.ref
/* 引用计数为1,
 * type为7,表明类型是数组,符合我们的预期。
 * gc_info为49154, 对应二进制1100 0000 0000 0010,紫色,在buf上的第2个位置
 */
$4 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 49154}, type_info = 3221356551}}}

代码中, a 有 两 个 元 素 , 来 看 第 二 个 元 素 a有两个元素,来看第二个元素 aa[1]进入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, 对应二进制1100 0000 0000 0011,紫色,在buf上的第3个位置。
 * 上一步$a[0]在第2个位置,两者刚好相邻。
 */
$7 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 49155}, type_info = 3221422087}}}

看下这个元素的具体内容,以确认它真的是我们的$a[1]

(gdb) p *(zend_array*)newRoot.ref
/* zend_array的详情
 * arData指向真实数据
 * nNumUsed=1:存了一个元素
 */
$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]
/*查看该元素内容,type为6,字串类型,符合预期*/
$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}

/*查看字串的存储 
 *  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"}
  
//打印字串具体内容,的确是$a[1]存储的字串
(gdb) p *((zend_array*)newRoot.ref).arData[0].val.value.str.val@8
$18 = "1_string"

3.2 垃圾回收算法

3.2.1 算法描述
  1. 遍历roots链表, 把当前元素标为灰色(zend_refcounted_h.gc_info置为GC_GREY),然后对当前元素的成员进行深度优先遍历,把成员的refcount减1,并且也标为灰色。(gc_mark_roots())
  2. 遍历roots链表中所有灰色元素及其子元素,如果发现其引用计数仍旧大于0,说明这个元素还在其他地方使用,那么将其颜色重新标记会黑色,并将其引用计数加1(在第一步有减1操作)。如果发现其引用计数为0,则将其标记为白色。(gc_scan_roots())
  3. 遍历roots链表,将黑色的元素从roots移除。然后对roots中颜色为白色的元素进行深度优先遍历,将其引用计数加1(在第一步有减1操作),同时将颜色为白色的子元素也加入roots链表。最后然后将roots链表移动到待释放的列表to_free中。(gc_collect_roots())
  4. 释放to_free列表的元素。
3.2.2 元素颜色转化图

image

3.2.3 为什么算法是有效的

为什么算法可以找到垃圾呢?我们知道,垃圾产生的原因就是循环引用,也就是说子元素指向了元素自身,使用引用计数无法清0。

算法的核心就是尝试遍历子元素,将其引用计数减1,若有循环引用的情况,则在减子元素引用计数后,必可使原始元素的引用计数清0。

再来回忆下第一节这样循环引用的图。
image
因为zend_array的子元素引用了自身,导致垃圾。

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

  1. 遍历zend_array,对arData中每个元素,将其引用计数-1。
  2. 遍历到第1个元素时,发现其指向引用类型。源码实现中有这样一段:
/php-7.0.14/Zend/zend_gc.c
/*如果是引用类型,则将它内部的zval对应的数据的引用计数减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)--;                            
        ...
    }
}

对应到我们的例子,就是将zend_array的引用计数减1,这时zend_array的引用计数就为0了,可以回收了!

3.2.4 核心代码解读

gc_possible_root()中调用了gc_collect_cycles()来进行垃圾回收。gc_collect_cycles是一个函数指针, 定义如下:

//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默认实现是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;
    /*
     *缓存冲区初始化时(gc_reset())设置了 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;
        
        
        /*如果已有回收活动正在进行,则返回*/
		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链表,对当前节点value的所有成员(如数组元素、成员属性)进行深度优先遍历把成员refcount减1
		gc_mark_roots();
		
		GC_TRACE("Scanning roots");
		/*遍历roots链表中所有灰色元素及其子元素,如果发现其引用计数仍旧大于0,说明这个元素还在其他地方使用,那么将其颜色重新标记会黑色,并将其引用计数加1。如果发现其引用计数为0,则将其标记为白色。*/
		gc_scan_roots();

		GC_TRACE("Collecting roots");
		additional_buffer = NULL;
		/*遍历roots链表,将黑色的元素从roots移除。
		 对roots中颜色为白色的元素进行深度优先遍历,将其引用计数加1,同时将颜色为白色的子元素也加入roots链表。
		 最后然后将roots链表移动到待释放的列表to_free中。
		 
		 关于additional_buffer,在3.2.5节中做详细说明
		 */
		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;
				...
				//调用free_obj释放对象
				if (obj->handlers->free_obj) {
					GC_REFCOUNT(obj)++;
					obj->handlers->free_obj(obj);
					GC_REFCOUNT(obj)--;
				}
				...
			} 
			//释放数组
			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队列*/
		current = to_free.next;
		while (current != &to_free) {
			next = current->next;
			p = current->ref;
			/*
			 *只有在原有垃圾缓存区的buffer才可以加入unused
			 *之所有有此判断,与additional_buffer相关
			 */
			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的内存
		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的用途

在执行gc_collect_roots()时,用到了gc_additional_buffer, 这个结构的用途是什么呢?
通过上面的说明我们知道,roots上存储了所有可能是垃圾的元素,但是并没有存放这些元素的子元素。在
在执行gc_collect_roots()时,我们做的很重要的一件事就是将所有白色元素放到roots链上,这当然也包括白色的子元素。子元素可能有很多,但受限于垃圾缓冲池的大小roots最长只有10000个,不够用怎么办呢?这时就需要临时申请额外的存储空间gc_additional_buffer。

3.2.5.2 gc_additional_buffer的结构

gc_additional_buffer结构如下

//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];
};

每个gc_additional_buffer中有GC_NUM_ADDITIONAL_ENTRIES个gc_root_buffer,可用于存储待回收的垃圾。当一个gc_additional_buffer不够用时,就会再申请一个gc_additional_buffer, 多个gc_additional_buffer使用next指针串连,形成链表。

image

3.2.5.3 gc_additional_buffer的具体使用

gc_additional_buffer在gc_add_garbage()中使用,gc_add_garbage的功能是将不在roots链上的白色元素挂接到roots链上。
调用栈如下:

gc_add_garbage()
gc_collect_white()
gc_collect_roots()

下面来具体看下gc_add_garbage的实现

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取一个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);
    }
    //现有垃圾回收池满了
    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或者当前additional_buffer已装满
        if (!*additional_buffer || (*additional_buffer)->used == GC_NUM_ADDITIONAL_ENTRIES) {
            //新申请内存装初始化一个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;
        }
        //从当前additional_buffe上取一个buffer
        buf = (*additional_buffer)->buf + (*additional_buffer)->used;
        (*additional_buffer)->used++;
        
        /*
         * 将buf位置记录为GC_ROOT_BUFFER_MAX_ENTRIES
         * 注意GC_ROOT_BUFFER_MAX_ENTRIES是不存在于原有垃圾缓冲区的一个位置
         */
        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结构

为什么gc要放在复杂变量的头部?为什么zval中有变量类型,gc中要再记录一份?

回忆一下php7中变量的存储方式
一个zval中包含一个zend_value结构,zend_value中相应类型的指针指向对应类型的实际存储空间。

一个array类形的变量,存储方式如下图所示。
image

zend_value中的arr指向了zend_array。

在垃圾处理过程中,我们主要用到的都是指向gc的指针。但是在染色时,我们需根据变量类型对变量内部存储的子元素。这时怎么办呢?看垃圾加收过程中的代码:

//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;
                //转换为zend_object类型
                zend_object *obj = (zend_object*)ref;
        
                ... ...
            }
            else if (GC_TYPE(ref) == IS_ARRAY) {
                ...
                //转换为zend_array类型
                ht = (zend_array*)ref;
                ...
            }
        }
    }
    ... ...
}

这段代码的功能是将元素及其子元素染成灰色,由gc_mark_roots()调用。它接收的参数ref变是一个指向gc的指针。

GC_TYPE(ref)用于获取变量类型。这也是为什么zval中记录了变量类型,我们仍然要在gc中冗余一份的原因。

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

因为gc在相应数据类型的起始位置,所以,在知道具体类型后,我们只要使用强制类型转换,就可以将指向gc类型的指向转为指向具体类型的指针,并通过类型指针取到变量具体数据。

zend_object *obj = (zend_object*)ref;
ht = (zend_array*)ref;
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值