0. gc的基本结构
0.1 zend_refcounted_h
在《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字节。
源码中,色值和地址的取设均采用了巧妙的位运算
//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之前,引用关系如下图所示:
unset之后引用关系如下图所示:
当执行unset操作后,$a所在的zval类型被标记为IS_UNDEF,zend_reference结构体的引用计数减1,但仍然大于0,这时,后面的结构就成为了垃圾,对此不处理会造成内存泄露。垃圾回收要处理的就是这种情况。
2. 进行垃圾回收的条件
如果一个变量value的refcount减少之后等于0,那么此value可以被释放掉,不属于垃圾。GC无需处理。
如果一个变量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. 垃圾回收机制
垃圾回收过程大致分为两步:
- 将可能是垃圾的变量记录到垃圾缓存buffer中
- 当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字节。
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(i])后,因为仍然有a[$i]对应的zend_array, 所以其引用计数不为0,会进入垃圾回收缓冲区。相应的垃圾收集器的状态如下图所示:
来看一下单个gc_root_buffer中存储的数据。我们知道,zend_array和zend_object结构的第一个字段都是gc,用于记录引用计数等与垃圾回收相关的数据。当一个变量可能成为垃圾时,其实gc_root_buffer并不是原样存储了一份变量相关的数据,而是用一个ref指针指向了变量数据对应的gc字段。
结合本例,gc_root_buffer.ref就是指向了zend_array.gc,如下图所示:
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=) 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=) 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[1]进入gc_possible_root的情况。
(gdb) c
Continuing.
unset 1
Breakpoint 1, gc_possible_root (ref=) 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 算法描述
- 遍历roots链表, 把当前元素标为灰色(zend_refcounted_h.gc_info置为GC_GREY),然后对当前元素的成员进行深度优先遍历,把成员的refcount减1,并且也标为灰色。(gc_mark_roots())
- 遍历roots链表中所有灰色元素及其子元素,如果发现其引用计数仍旧大于0,说明这个元素还在其他地方使用,那么将其颜色重新标记会黑色,并将其引用计数加1(在第一步有减1操作)。如果发现其引用计数为0,则将其标记为白色。(gc_scan_roots())
- 遍历roots链表,将黑色的元素从roots移除。然后对roots中颜色为白色的元素进行深度优先遍历,将其引用计数加1(在第一步有减1操作),同时将颜色为白色的子元素也加入roots链表。最后然后将roots链表移动到待释放的列表to_free中。(gc_collect_roots())
- 释放to_free列表的元素。
3.2.2 元素颜色转化图
3.2.3 为什么算法是有效的
为什么算法可以找到垃圾呢?我们知道,垃圾产生的原因就是循环引用,也就是说子元素指向了元素自身,使用引用计数无法清0。
算法的核心就是尝试遍历子元素,将其引用计数减1,若有循环引用的情况,则在减子元素引用计数后,必可使原始元素的引用计数清0。
再来回忆下第一节这样循环引用的图。
因为zend_array的子元素引用了自身,导致垃圾。
我们看看算法是如何清除垃圾的:
- 遍历zend_array,对arData中每个元素,将其引用计数-1。
- 遍历到第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指针串连,形成链表。
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类形的变量,存储方式如下图所示。
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;