乐趣区

PHP源码学习20190401-PHP垃圾回收1

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 个存储单元之后,垃圾回收算法就会启动,对缓冲区中的所有疑似垃圾进行标记与清除,垃圾回收算法的过程会在下一篇笔记进行讲解。
退出移动版