4

baiyan

全部视频:https://segmentfault.com/a/11...

垃圾回收触发条件

  • 我们知道,在PHP中,如果一个变量的引用计数减少到0(没有任何地方在使用这个变量),它所占用的内存就会被PHP虚拟机自动回收,并不会被当做垃圾。垃圾回收的触发条件是当一个变量的引用计数的值减少1之后,仍不为0(还有某个地方在使用这个变量),才有可能是垃圾。需要让我们人工去对其进行进一步的检验,看它是否真的是垃圾,然后再做后续的操作。一个典型的例子就是在我们使用数组对象的过程中可能存在的循环引用问题。它会让某个变量自己引用自己。看下面一个例子:
<?php
$a = ['time' => time()];
$a[] = &$a; //循环引用
unset($a);
  • 我们可以知道,unset($a)之后,$a的type类型变成了0(IS_UNDEF),同时其指向的zend_reference结构体的refcount变为了1(因为$a数组中的元素仍然在引用它),我们画图来表示一下现在的内存情况:

  • 那么问题出现了,\$a是unset掉了,但是由于原始的zend_array中的元素仍然在指向仍然在指向zend_reference结构体,所以zend_reference的refcount是1,而并非是预期的0。这样一来,这两个zend_reference与zend_array结构在unset($a)之后,仍然存在于内存之中,如果对此不作任何处理,就会造成内存泄漏
  • 以上详细的讲解请看:【PHP源码学习】2019-03-19 PHP引用
  • 那么如何解决循环引用带来的内存泄漏问题呢?我们的垃圾回收就要派上用场了。
  • 在PHP7中,垃圾回收分为垃圾回收器垃圾回收算法两大部分
  • 在这篇笔记中只讲解第一部分:垃圾回收器

垃圾回收器

  • 在PHP7中,如果检测到refcount减1后仍大于0的变量,会首先把它放入一个双向链表中,它就是我们的垃圾回收器。这个垃圾回收器相当于一个缓冲区的作用,待缓冲区满了之后,等待垃圾回收算法进行后续的标记与清除操作。
  • 垃圾回收算法的启动时机并不是简单的有一个疑似垃圾到来,就要运行一次,而是待缓冲区存满了之后(规定10001个存储单元),然后垃圾回收算法才会启动,对缓冲区中的疑似垃圾进行最终的标记和清除。这个垃圾回收器缓冲区的作用就是减少垃圾回收算法运行的频率,减少对操作系统资源的占用以及对正在运行的服务端代码的影响,下面我们通过代码来详细讲解。

垃圾回收器存储结构

  • 垃圾回收器的结构如下:
typedef struct _gc_root_buffer {
    zend_refcounted          *ref;          
    struct _gc_root_buffer   *next;     //双向链表,指向下一个缓冲区单元
    struct _gc_root_buffer   *prev;     //双向链表,指向上一个缓冲区单元
    uint32_t                 refcount;
} gc_root_buffer;
  • 垃圾回收器是一个双向链表,那么如何维护这个双向链表首尾指针的信息,还有缓冲区的使用情况等额外信息呢,现在就需要使用我们的全局变量zend_gc_globals了:
typedef struct _zend_gc_globals {
    zend_bool         gc_enabled;         //是否启用gc
    zend_bool         gc_active;          //当前是否正在运行gc
    zend_bool         gc_full;              //缓冲区是否满了

    gc_root_buffer   *buf;                  /*指向缓冲区头部 */
    gc_root_buffer    roots;              /*当前处理的垃圾缓冲区单元,注意这里不是指针*/
    gc_root_buffer   *unused;              /*指向未使用的缓冲区单元链表开头(用于串联缓冲区碎片)*/
    gc_root_buffer   *first_unused;          /*指向第一个未使用的缓冲区单元*/
    gc_root_buffer   *last_unused;          /*指向最后一个未使用的缓冲区单元 */

    gc_root_buffer    to_free;            
    gc_root_buffer   *next_to_free;
    ...
    
} zend_gc_globals;

垃圾回收器初始化

  • 那么现在,我们需要为垃圾回收器分配内存空间,以存储接下来可能到来的可疑垃圾,我们通过gc_init()函数实现空间的分配:
ZEND_API void gc_init(void)
{
    if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
        GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);
        GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
        gc_reset();
    }
}
  • GC_G这个宏是取得以上zend_gc_globals结构体中的变量。我们现在还没有生成缓冲区,所以进入这个if分支。通过系统调用malloc分配一块内存,这个内存的大小是单个缓冲区结构体的大小 * 10001:
#define GC_ROOT_BUFFER_MAX_ENTRIES 10001
  • 那么现在我们得到了大小为10001的缓冲区(第1个单元不用),并把指针的步长置为gc_root_buffer类型,随后将它的last_unused指针指向缓冲区的末尾,然后通过gc_reset()做一些初始化操作:
ZEND_API void gc_reset(void)
{
    GC_G(gc_runs) = 0;
    GC_G(collected) = 0;
    GC_G(gc_full) = 0;
    ...

    GC_G(roots).next = &GC_G(roots);
    GC_G(roots).prev = &GC_G(roots);

    GC_G(to_free).next = &GC_G(to_free);
    GC_G(to_free).prev = &GC_G(to_free);

    if (GC_G(buf)) {                         //由于我们之前分配了缓冲区,进这里
        GC_G(unused) = NULL;                 //没有缓冲区碎片,置指针为NULL
        GC_G(first_unused) = GC_G(buf) + 1;  //将指向第一个未使用空间的指针往后挪1个单元的长度
    } else {
        GC_G(unused) = NULL;
        GC_G(first_unused) = NULL;
        GC_G(last_unused) = NULL;
    }

    GC_G(additional_buffer) = NULL;
}
  • 根据这个函数中的内容,我们可以画出当前的内存结构图:

将疑似垃圾存入垃圾回收器

  • 这样一来,我们垃圾回收器缓冲区就初始化完毕了,现在等着zend虚拟机收集可能会是垃圾的变量,存入这些缓冲区中,这步操作通过gc_possible_root(zend_refcounted *ref)函数完成:
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;
    }

    ZEND_ASSERT(GC_TYPE(ref) == IS_ARRAY || GC_TYPE(ref) == IS_OBJECT);
    ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK));
    ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref)));

    GC_BENCH_INC(zval_possible_root);

    newRoot = GC_G(unused);
    if (newRoot) {
        GC_G(unused) = newRoot->prev;
    } else if (GC_G(first_unused) != GC_G(last_unused)) {
        newRoot = GC_G(first_unused);
        GC_G(first_unused)++;
    } else {
        if (!GC_G(gc_enabled)) {
            return;
        }
        GC_REFCOUNT(ref)++;
        gc_collect_cycles();
        GC_REFCOUNT(ref)--;
        if (UNEXPECTED(GC_REFCOUNT(ref)) == 0) {
            zval_dtor_func(ref);
            return;
        }
        if (UNEXPECTED(GC_INFO(ref))) {
            return;
        }
        newRoot = GC_G(unused);
        if (!newRoot) {
#if ZEND_GC_DEBUG
            if (!GC_G(gc_full)) {
                fprintf(stderr, "GC: no space to record new root candidate\n");
                GC_G(gc_full) = 1;
            }
#endif
            return;
        }
        GC_G(unused) = newRoot->prev;
    }

    GC_TRACE_SET_COLOR(ref, GC_PURPLE);
    GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
    newRoot->ref = ref;

    newRoot->next = GC_G(roots).next;
    newRoot->prev = &GC_G(roots);
    GC_G(roots).next->prev = newRoot;
    GC_G(roots).next = newRoot;

    GC_BENCH_INC(zval_buffered);
    GC_BENCH_INC(root_buf_length);
    GC_BENCH_PEAK(root_buf_peak, root_buf_length);
}
  • 代码有点长不要紧,我们逐行分析。首先又声明了一个指向缓冲区的指针newRoot。接下来判断如果垃圾回收器已经运行,那么本次就不再执行了。然后将zend_gc_globals全局变量上的unused指针字段赋值给newRoot指针,然而unused指针为NULL(因为没有缓冲区碎片),所以newRoot此时也为NULL。故接下来进入else if分支:
    newRoot = GC_G(first_unused);
    GC_G(first_unused)++;
  • 首先将newRoot指向第一个未使用的缓冲区单元,所以下一行需要将第一个未使用的缓冲区单元往后挪一个单元,方便下一次的使用,很好理解,跳过这个长长的else分支往下继续执行:
    GC_TRACE_SET_COLOR(ref, GC_PURPLE);
    GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
    newRoot->ref = ref;

    newRoot->next = GC_G(roots).next;
    newRoot->prev = &GC_G(roots);
    GC_G(roots).next->prev = newRoot;
    GC_G(roots).next = newRoot;
  • 第一行GC_TRACE这个宏用来打印相关DEBUG信息,我们略过这一行。
  • 第二行执行GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;我们看到这里有一个GC_PURPLE,也就是颜色的概念。在PHP垃圾回收中,用到了4种颜色:
#define GC_BLACK  0x0000
#define GC_WHITE  0x8000
#define GC_GREY   0x4000
#define GC_PURPLE 0xc000
  • 源码中对它们的解释如下:
 * BLACK  (GC_BLACK)   - In use or free.
 * GREY   (GC_GREY)    - Possible member of cycle.
 * WHITE  (GC_WHITE)   - Member of garbage cycle.
 * PURPLE (GC_PURPLE)  - Possible root of cycle.
  • 这里我们先不对每一种颜色做详细解释。我们用(newRoot - GC_G(buf)) | GC_PURPLE的意思是:newRoot - GC_G(buf)(缓冲区起始地址)代表当前使用的缓冲区的偏移量,再与0xc000做或运算,结果拼装到变量的gc_info字段中,这个字段是一个uint16类型,所以可以利用前2位把它标记成紫色,同时利用后14位存储偏移量。最终字段按位拆开的情况如图:

  • 第三行:将当前引用赋值到当前缓冲区中
  • 接下来是双向链表的指针操作:
    newRoot->next = GC_G(roots).next;
    newRoot->prev = &GC_G(roots);
    GC_G(roots).next->prev = newRoot;
    GC_G(roots).next = newRoot;
  • 其目的是将当前缓冲区的prev和next指针指向全局变量中的root字段,同时将全局变量中的root字段的prev与next指针指向当前使用的缓冲区。

  • 至此,我们就可以将所有疑似垃圾的变量都放到缓冲区中,一直存下去,待存满缓冲区10000个存储单元之后,垃圾回收算法就会启动,对缓冲区中的所有疑似垃圾进行标记与清除,垃圾回收算法的过程会在下一篇笔记进行讲解。

NoSay
449 声望544 粉丝