【PHP7源码学习】 PHP垃圾回收(下)

垃圾回收器缓冲区元素的存储

在上一篇文章【PHP源码学习】PHP垃圾回收(上)中,我们将所有疑似垃圾的元素都放到垃圾回收器缓冲区中,一直存下去,待存满垃圾回收器缓冲区10000个存储单元之后,垃圾回收算法就会启动,对缓冲区中的所有疑似垃圾进行标记与清除。垃圾回收算法需要对这个缓冲区进行扫描遍历,判定每一个存储单元中的内容是否为最终的垃圾。

回顾gc_possible_root()函数,我们将疑似垃圾存入了一个大小为10000的缓冲区中:

ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
{
...
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)--;
...
}
}

由此可见,垃圾回收算法会在缓冲区存满的时候才会启动,这样会减少垃圾回收算法运行的频率,进而减小性能的损耗且提高效率(详见上一篇文章)

垃圾回收算法的运行

在此之前,我们已经将垃圾缓冲区内的所有元素均标记成了紫色GC_PURPLE

我们在gc_possible_root()函数中调用了gc_collect_cycles()函数,这个gc_collect_cycles是一个函数指针,实际指向的是zend_gc_collect_cycles()函数,最终会调用它:

ZEND_API int zend_gc_collect_cycles(void)
{

...
//遍历垃圾回收器链表,如果垃圾回收器链表中的元素是紫色,对其进行深度优先遍历并将refcount减1,并把它们标记为灰色GC_GREY
gc_mark_roots();

//遍历垃圾回收器链表,如果垃圾回收器链表中的元素是灰色,那么判断每个疑似垃圾的元素的refcount是否>0,如果>0就不是垃圾,将其标记为黑色GC_BLACK并且将refcount+1恢复到原来的值;否则为垃圾,将其标记为白色GC_WHITE,它们是真正的垃圾,后面需要释放掉
gc_scan_roots();
...

//把垃圾回收器中为白色GC_WHITE的垃圾单独摘出来放到to_free垃圾链表中,并删除垃圾回收器中不是垃圾的元素
count = gc_collect_roots(&gc_flags);

GC_G(gc_active) = 0;

if (GC_G(to_free).next == &GC_G(to_free)) {
/* 如果垃圾链表to_free为空,那么就没有需要释放的垃圾,直接返回 */
GC_TRACE("Nothing to free");
return 0;
}

/* 将全局变量中的垃圾链表拷贝到局部变量中 */
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;

/* 释放全局的垃圾链表 */
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);

...

if (gc_flags & GC_HAS_DESTRUCTORS) {
GC_TRACE("Calling destructors");

/* 在析构之前记录引用计数*/
current = to_free.next;
while (current != &to_free) {
current->refcount = GC_REFCOUNT(current->ref);
current = current->next;
}

/* 析构对象*/
current = to_free.next;
while (current != &to_free) {
p = current->ref;
GC_G(next_to_free) = current->next;
if (GC_TYPE(p) == IS_OBJECT) {
zend_object *obj = (zend_object*)p;

if (!(GC_FLAGS(obj) & IS_OBJ_DESTRUCTOR_CALLED)) {
GC_TRACE_REF(obj, "calling destructor");
GC_FLAGS(obj) |= IS_OBJ_DESTRUCTOR_CALLED;
if (obj->handlers->dtor_obj
&& (obj->handlers->dtor_obj != zend_objects_destroy_object
|| obj->ce->destructor)) {
GC_REFCOUNT(obj)++;
obj->handlers->dtor_obj(obj);
GC_REFCOUNT(obj)--;
}
}
}
current = GC_G(next_to_free);
}

/* 销毁在对象析构过程中出现的值 */
current = to_free.next;
while (current != &to_free) {
GC_G(next_to_free) = current->next;
if (GC_REFCOUNT(current->ref) > current->refcount) {
gc_remove_nested_data_from_buffer(current->ref, current);
}
current = GC_G(next_to_free);
}
}

/* 销毁zval */
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");
if (GC_TYPE(p) == IS_OBJECT) { //释放对象
zend_object *obj = (zend_object*)p;

EG(objects_store).object_buckets[obj->handle] = SET_OBJ_INVALID(obj);
GC_TYPE(obj) = IS_NULL;
if (!(GC_FLAGS(obj) & IS_OBJ_FREE_CALLED)) {
GC_FLAGS(obj) |= IS_OBJ_FREE_CALLED;
if (obj->handlers->free_obj) {
GC_REFCOUNT(obj)++;
obj->handlers->free_obj(obj);
GC_REFCOUNT(obj)--;
}
}
SET_OBJ_BUCKET_NUMBER(EG(objects_store).object_buckets[obj->handle], EG(objects_store).free_list_head);
EG(objects_store).free_list_head = obj->handle;
p = current->ref = (zend_refcounted*)(((char*)obj) - obj->handlers->offset);
} else if (GC_TYPE(p) == IS_ARRAY) { //释放数组
zend_array *arr = (zend_array*)p;

GC_TYPE(arr) = IS_NULL;

/* GC may destroy arrays with rc>1. This is valid and safe. */
HT_ALLOW_COW_VIOLATION(arr);

zend_hash_destroy(arr);
}
current = GC_G(next_to_free);
}

/* 释放垃圾所占用内存空间 */
current = to_free.next;
while (current != &to_free) {
next = current->next;
p = current->ref;
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;
}

while (GC_G(additional_buffer) != additional_buffer_snapshot) {
gc_additional_buffer *next = GC_G(additional_buffer)->next;
efree(GC_G(additional_buffer)); //通过efree()释放内存空间
GC_G(additional_buffer) = next;
}

/* 收尾 */
GC_TRACE("Collection finished");
GC_G(collected) += count;
GC_G(next_to_free) = orig_next_to_free;
#if ZEND_GC_DEBUG
GC_G(gc_full) = orig_gc_full;
#endif
GC_G(gc_active) = 0;
}

return count;
}

在第一步的gc_mark_roots()中,调用了gc_mark_grey(),标记为灰色:

static void gc_mark_roots(void)
{
gc_root_buffer *current = GC_G(roots).next;

while (current != &GC_G(roots)) {
if (GC_REF_GET_COLOR(current->ref) == GC_PURPLE) {
gc_mark_grey(current->ref);
}
current = current->next;
}
}

在第二步的gc_scan_roots()中,调用了gc_scan(),进而又调用了gc_mark_black(),标记为黑色,而标记为白色没有调用函数:

static void gc_scan_roots(void)
{
gc_root_buffer *current = GC_G(roots).next;

while (current != &GC_G(roots)) {
gc_scan(current->ref); //内部还会调用gc_mark_black()
current = current->next;
}
}

下面我们总结一下垃圾回收算法的执行流程:

 - 调用函数gc_mark_roots();首先对垃圾回收器进行深度优先遍历,将refcount-1,并标记为灰色
- 调用函数gc_scan_roots();再次对垃圾回收器进行深度优先遍历,判断当前的refcount,如果大于0,就不是垃圾,标记为黑色,并且要将refcount+1恢复到原来的值;如果等于0,就是垃圾,标记为白色
- 调用函数gc_collect_roots(),将白色的垃圾从垃圾回收器中摘出来,放到垃圾链表中并等待回收;同时将垃圾回收器中不是垃圾的元素移除,以节省垃圾回收器空间
- 释放全局垃圾链表,拷贝到局部垃圾链表,提高效率
- 遍历局部垃圾链表
- 首先析构对象
- 销毁在对象析构过程中出现的值
- 销毁对象与数组的zval
- 释放垃圾所占用内存空间
- 收尾工作

那么对其进行深度优先遍历并标记的代码都类似,举一个标记灰色的例子:

static void gc_mark_grey(zend_refcounted *ref)
{
HashTable *ht;
Bucket *p, *end;
zval *zv;

tail_call:
if (GC_REF_GET_COLOR(ref) != GC_GREY) {
ht = NULL;
GC_BENCH_INC(zval_marked_grey);
GC_REF_SET_COLOR(ref, GC_GREY);

if (GC_TYPE(ref) == IS_OBJECT) {
zend_object_get_gc_t get_gc;
zend_object *obj = (zend_object*)ref;

if (EXPECTED(!(GC_FLAGS(ref) & IS_OBJ_FREE_CALLED) &&
(get_gc = obj->handlers->get_gc) != NULL)) {
int n;
zval *zv, *end;
zval tmp;

ZVAL_OBJ(&tmp, obj);
ht = get_gc(&tmp, &zv, &n);
end = zv + n;
if (EXPECTED(!ht)) {
if (!n) return;
while (!Z_REFCOUNTED_P(--end)) {
if (zv == end) return;
}
}
while (zv != end) {
if (Z_REFCOUNTED_P(zv)) {
ref = Z_COUNTED_P(zv);
GC_REFCOUNT(ref)--;
gc_mark_grey(ref);
}
zv++;
}
if (EXPECTED(!ht)) {
ref = Z_COUNTED_P(zv);
GC_REFCOUNT(ref)--;
goto tail_call;
}
} else {
return;
}
} else if (GC_TYPE(ref) == IS_ARRAY) {
if (((zend_array*)ref) == &EG(symbol_table)) {
GC_REF_SET_BLACK(ref);
return;
} else {
ht = (zend_array*)ref;
}
} else if (GC_TYPE(ref) == IS_REFERENCE) {
if (Z_REFCOUNTED(((zend_reference*)ref)->val)) {
ref = Z_COUNTED(((zend_reference*)ref)->val);
GC_REFCOUNT(ref)--;
goto tail_call;
}
return;
} else {
return;
}

if (!ht->nNumUsed) return;
p = ht->arData;
end = p + ht->nNumUsed;
while (1) {
end--;
zv = &end->val;
if (Z_TYPE_P(zv) == IS_INDIRECT) {
zv = Z_INDIRECT_P(zv);
}
if (Z_REFCOUNTED_P(zv)) {
break;
}
if (p == end) return;
}
while (p != end) {
zv = &p->val;
if (Z_TYPE_P(zv) == IS_INDIRECT) {
zv = Z_INDIRECT_P(zv);
}
if (Z_REFCOUNTED_P(zv)) {
ref = Z_COUNTED_P(zv);
GC_REFCOUNT(ref)--;
gc_mark_grey(ref);
}
p++;
}
zv = &p->val;
if (Z_TYPE_P(zv) == IS_INDIRECT) {
zv = Z_INDIRECT_P(zv);
}
ref = Z_COUNTED_P(zv);
GC_REFCOUNT(ref)--;
goto tail_call;
}
}

我们可以看到,该函数中用到了大量的递归调用。我们知道,垃圾回收就是用来解决循环引用的问题。循环引用的问题是自己引用自己,那么究竟在哪里才引用了自己,就需要进行深度优先遍历。举个例子,数组中的元素可能又是一个数组,里面的数组元素可能还是一个数组,间接引用了好多层,最后一层才引用了外层数组本身。那么,这种情况下,就需要不断地对其进行遍历并标记,直到找到最终最深的引用位置之前,所有中间的间接引用层都是垃圾,我们都需要对其进行标记并且清除。

比如之前讲过的循环引用示例:

【PHP7源码学习】 PHP垃圾回收(下)

这个例子很简单,只是嵌套了一层就引用回了自己。如果bucket中又是一个数组,它就会再次指向一个zend_array,而这个新zend_array又指向了一个bucket……等等,我们可以无限层嵌套下去,直到最后一层,才是深度优先遍历的终点。所以,我们在这个过程中所遍历的zend_array与bucket,全部都是垃圾,需要我们去标记与清除。

观察上图,我们可以注意到,在这个遍历过程中,所有的结构(zend_array或bucket等)的refcount都为1,这就证明了在垃圾回收算法中,为什么要先将refcount-1,然后判断是否大于0,如果小于等于0,那么他们就是垃圾,需要回收;如果大于0,则说明还有其他地方引用这个结构,那么他们不是垃圾,不需要回收。

我们最后总结一下颜色的变化:紫色 -> 灰色 -> 黑色或白色

- 紫色:代表该元素已经放入垃圾回收器
- 灰色:代表该元素已经经过了refcount-1的操作
- 黑色:不是垃圾,要将refcount+1还原
- 白色:是垃圾,等待后续回收

未经允许不得转载:PHP100中文网 - 中国第一档PHP资源分享门户 » 【PHP7源码学习】 PHP垃圾回收(下)

赞 (0) 打赏

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏