PHP源码学习20190402-PHP垃圾回收2

22次阅读

共计 7100 个字符,预计需要花费 18 分钟才能阅读完成。

baiyan

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

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

  • 在上一篇文章【PHP 源码学习】2019-04-01 PHP 垃圾回收 1 中,我们将所有疑似垃圾的元素都放到垃圾回收器缓冲区中,一直存下去,待存满垃圾回收器缓冲区 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;
    }
}
  • 我们可以看到,该函数中用到了大量的递归调用。我们知道,垃圾回收就是用来解决循环引用的问题。循环引用的问题是自己引用自己,那么究竟在哪里才引用了自己,就需要进行深度优先遍历。举个例子,数组中的元素可能又是一个数组,里面的数组元素可能还是一个数组,间接引用了好多层,最后一层才引用了外层数组本身。那么,这种情况下,就需要不断地对其进行遍历并标记,直到找到最终最深的引用位置之前,所有中间的间接引用层都是垃圾,我们都需要对其进行标记并且清除。
  • 比如之前讲过的循环引用示例:

  • 这个例子很简单,只是嵌套了一层就引用回了自己。如果 bucket 中又是一个数组,它就会再次指向一个 zend_array,而这个新 zend_array 又指向了一个 bucket…… 等等,我们可以无限层嵌套下去,直到最后一层,才是深度优先遍历的终点。所以,我们在这个过程中所遍历的 zend_array 与 bucket,全部都是垃圾,需要我们去标记与清除。
  • 观察上图,我们可以注意到,在这个遍历过程中,所有的结构 (zend_array 或 bucket 等) 的 refcount 都为 1,这就证明了在垃圾回收算法中,为什么要先将 refcount-1,然后判断是否大于 0,如果小于等于 0,那么他们就是垃圾,需要回收;如果大于 0,则说明还有其他地方引用这个结构,那么他们不是垃圾,不需要回收。
  • 我们最后总结一下颜色的变化:紫色 -> 灰色 -> 黑色或白色
- 紫色:代表该元素已经放入垃圾回收器
- 灰色:代表该元素已经经过了 refcount- 1 的操作
- 黑色:不是垃圾,要将 refcount+ 1 还原
- 白色:是垃圾,等待后续回收

正文完
 0