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