PHP内核之旅-6.垃圾回收机制

时间:2021-12-11 17:14:06

回收PHP 内核之旅系列

一、引用计数

只有使用引用计数的变量才需要回收。引用计数就是用来标记变量的引用次数的。

当有新的变量zval指向value时,计数器加1,当变量zval销毁时,计数器减一。当引用计数为0时,表示此value没有被任何变量指向,可以对value进行释放。

下面的例子说明引用计数的是如何变化的:

$x = array(); //array这个value被变量$x引用1次,refcount = 1
$y = $x; //array这个value被变量$x,$y分别引用1次,refcount = 2
$z = $y; //array这个value被变量$x,$y,$z分别引用1次,refcount = 3
unset($y); //array这个value被变量$x,$z分别引用1次,refcount = 2,$y被销毁了,没有引用array这个value

使用引用计数的类型有以下几种:

string、array、object、resource、reference

下面的表格说明了只有type_flag为以下8种类型且IS_TYPE_REFOUNTED=true的变量才使用引用计数

  type_flag IS_TYPE_REFCOUNTED
1         simple types  
2 string true
3 interned string  
4 array true
5 immutable array  
6 object true
7 resource true
8 reference true

1.正常回收场景:

a.自动回收

  在zval断开value的指向时,如果发现refcount=0则会直接释放value。

    断开value指向的情形:

    (1)修改变量时会断开原有value的指向

    (2)函数返回时会释放所有的局部变量

b.主动回收

  unset()函数

2.垃圾回收场景:

当因循环引用导致无法释放的变量称为垃圾,用垃圾回收器进行回收。

注意:

(1)如果一个变量value的refcount减一之后等于0,此value可以被释放掉,不属于垃圾。垃圾回收器不会处理。

(2)如果一个变量value的refcount减一之后还是大于0,此value被认为不能被释放掉,可能成为一个垃圾。

(3)垃圾回收器会将可能的垃圾收集起来,等达到一定数量后开始启动垃圾鉴定程序,把真正的垃圾释放掉。

(4)收集的时机是refount减少时。

(5)收集到的垃圾保存到一个buffer缓冲区中。

(6)垃圾只会出现在array、object类型中。

二、回收原理

1.垃圾是如何回收的

垃圾收集器收集的可能垃圾到达一定数量后,启动垃圾鉴定、回收程序。

2.垃圾鉴定

垃圾是由于成员引用自身导致的,那么就对value的refcount减一操作,如果value的refount变为了0,则表明其引用全部来自自身成员,value属于垃圾。

3.垃圾回收的步骤

PHP内核之旅-6.垃圾回收机制

步骤一:遍历垃圾回收器的buffer缓冲区,把value标为灰色,把value的成员的refount-1,标为白色。

步骤二:遍历垃圾回收器的buffer缓冲区,如果value的 refcount等于0,则认为是垃圾,标为白色;如果不等于0,则表示还有外部的引用,不是垃圾,将refcount+1还原回去,标为黑色。

步骤三:遍历垃圾回收器的buffer缓冲区,将value为非白色的节点从buffer中删除,最终buffer缓冲区中都是真正的垃圾。

步骤四:遍历垃圾回收器的buffer缓冲区,释放此value。

三、代码实现

1.垃圾管家

_zend_gc_globals 对垃圾进行管理,收集到的可能成为垃圾的value就保存在这个结构的buf中,称为垃圾缓存区。
文件路劲:\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; //预分配的垃圾缓存区,用于保存可能成为垃圾的value
gc_root_buffer roots; //指向buf中最新加入的一个可能垃圾
gc_root_buffer *unused; //指向buf中没有使用的buffer
gc_root_buffer *first_unused; //指向第一个没有使用的buffer
gc_root_buffer *last_unused; //指向最后一个没有使用的buffer gc_root_buffer to_free; //待释放的垃圾
gc_root_buffer *next_to_free; //下指向下一个待释放的垃圾 uint32_t gc_runs; //统计GC运行次数
uint32_t collected; //统计已回收的垃圾数 #if GC_BENCH
uint32_t root_buf_length;
uint32_t root_buf_peak;
uint32_t zval_possible_root;
uint32_t zval_buffered;
uint32_t zval_remove_from_buffer;
uint32_t zval_marked_grey;
#endif gc_additional_buffer *additional_buffer; } zend_gc_globals;

_zend_gc_globals

2.垃圾管家初始化

(1)php.ini解析后调用gc_init()初始垃圾管家_zend_gc_globals

文件路径:\Zend\zend_gc.c

 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_ROOT_BUFFER_MAX_ENTRIES=10001
GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
gc_reset();
}
}

gc_init

(2)gc_init()函数里面调用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); GC_G(unused) = NULL;
GC_G(first_unused) = NULL;
GC_G(last_unused) = NULL; GC_G(additional_buffer) = NULL;
}

gc_reset

3.判断是否需要收集

(1)在销毁一个变量时就会判断是否需要收集。调用i_zval_ptr_dtor()函数

文件路径:Zend\zend_variables.h

 static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC)
{
if (Z_REFCOUNTED_P(zval_ptr)) {//type_flags & IS_TYPE_REFCOUNTED
zend_refcounted *ref = Z_COUNTED_P(zval_ptr);
if (!--GC_REFCOUNT(ref)) {//refcount - 1 之后等于0,则不是垃圾,正常回收
_zval_dtor_func(ref ZEND_FILE_LINE_RELAY_CC);
} else {//如果refcount - 1 之后仍然大于0,垃圾管家进行收集
gc_check_possible_root(ref);
}
}
}

i_zval_ptr_dtor

(2)如果refcount减一后,refcount等于0,则认为不是垃圾,释放此value

 //文件路径:\Zend\zend_variables.c
ZEND_API void ZEND_FASTCALL _zval_dtor_func(zend_refcounted *p ZEND_FILE_LINE_DC)
{
switch (GC_TYPE(p)) {
case IS_STRING:
case IS_CONSTANT: {
zend_string *str = (zend_string*)p;
CHECK_ZVAL_STRING_REL(str);
zend_string_free(str);
break;
}
case IS_ARRAY: {
zend_array *arr = (zend_array*)p;
zend_array_destroy(arr);
break;
}
case IS_CONSTANT_AST: {
zend_ast_ref *ast = (zend_ast_ref*)p; zend_ast_destroy_and_free(ast->ast);
efree_size(ast, sizeof(zend_ast_ref));
break;
}
case IS_OBJECT: {
zend_object *obj = (zend_object*)p; zend_objects_store_del(obj);
break;
}
case IS_RESOURCE: {
zend_resource *res = (zend_resource*)p; /* destroy resource */
zend_list_free(res);
break;
}
case IS_REFERENCE: {
zend_reference *ref = (zend_reference*)p; i_zval_ptr_dtor(&ref->val ZEND_FILE_LINE_RELAY_CC);
efree_size(ref, sizeof(zend_reference));
break;
}
default:
break;
}
}

_zval_dtor_func

(3)如果refcount减一后,refcount大于0,则认为value可能是垃圾,垃圾管家进行收集

 \\文件路径:\Zend\zend_gc.h
static zend_always_inline void gc_check_possible_root(zend_refcounted *ref)
{
if (GC_TYPE(ref) == IS_REFERENCE) {
zval *zv = &((zend_reference*)ref)->val; if (!Z_REFCOUNTED_P(zv)) {
/*
Z_TYPE_FLAGS 与 IS_TYPE_REFCOUNTED 与运算后,不等于0,则会被释放掉
Z_REFCOUNTED_P --> ((Z_TYPE_FLAGS(zval) & IS_TYPE_REFCOUNTED) != 0)
Z_TYPE_FLAGS(zval) --> (zval).u1.v.type_flags
IS_TYPE_REFCOUNTED -> 1<<2 (0100)
*/
return;
}
ref = Z_COUNTED_P(zv); //Z_COUNTED_P --> (zval).value.counted GC头部
}
if (UNEXPECTED(GC_MAY_LEAK(ref))) {
gc_possible_root(ref); //垃圾管家收集可能的垃圾
}
}

gc_check_possible_root

4.收集垃圾

 \\文件路径:\Zend\zend_gc.c
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)); // 只收集颜色为GC_BLACK的变量
ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref))); GC_BENCH_INC(zval_possible_root); newRoot = GC_G(unused); //拿出unused指向的节点
if (newRoot) { //如果拿出的节点是可用的,则将unused指向下一个节点
GC_G(unused) = newRoot->prev;
} else if (GC_G(first_unused) != GC_G(last_unused)) {//如果unused没有可用的,且first_unused还没有推进到last_unused,则表示buf缓存区中还有可用的节点
newRoot = GC_G(first_unused); //拿出first_unused指向的节点
GC_G(first_unused)++; //first_unused指向下一个节点
} else {//buf缓存区已满,启动垃圾鉴定、垃圾回收
if (!GC_G(gc_enabled)) { //如果未启用垃圾回收,则直接返回
return;
}
GC_REFCOUNT(ref)++;
gc_collect_cycles();
GC_REFCOUNT(ref)--;
if (UNEXPECTED(GC_REFCOUNT(ref)) == ) {
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) = ;
}
#endif
return;
}
GC_G(unused) = newRoot->prev;
} GC_TRACE_SET_COLOR(ref, GC_PURPLE); //将插入的变量标为紫色,防止重复插入
//将该节点在buf数组中的位置保存到了gc_info中,当后续value的refcount变为了0,
//需要将其从buf中删除时可以知道该value保存在哪个gc_root_buffer中
GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
newRoot->ref = ref; //插入roots链表头部
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);
}

gc_possible_root

5.释放垃圾

 ZEND_API int zend_gc_collect_cycles(void)
{
int count = ; 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 = ;
gc_additional_buffer *additional_buffer_snapshot;
#if ZEND_GC_DEBUG
zend_bool orig_gc_full;
#endif if (GC_G(gc_active)) {
return ;
} GC_TRACE("Collecting cycles");
GC_G(gc_runs)++;
GC_G(gc_active) = ; GC_TRACE("Marking roots");
gc_mark_roots();
GC_TRACE("Scanning roots");
gc_scan_roots(); #if ZEND_GC_DEBUG
orig_gc_full = GC_G(gc_full);
GC_G(gc_full) = ;
#endif GC_TRACE("Collecting roots");
additional_buffer_snapshot = GC_G(additional_buffer);
count = gc_collect_roots(&gc_flags);
#if ZEND_GC_DEBUG
GC_G(gc_full) = orig_gc_full;
#endif
GC_G(gc_active) = ; if (GC_G(to_free).next == &GC_G(to_free)) {
/* nothing to free */
GC_TRACE("Nothing to free");
return ;
} /* 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); #if ZEND_GC_DEBUG
orig_gc_full = GC_G(gc_full);
GC_G(gc_full) = ;
#endif if (gc_flags & GC_HAS_DESTRUCTORS) {
GC_TRACE("Calling destructors"); /* Remember reference counters before calling destructors */
current = to_free.next;
while (current != &to_free) {
current->refcount = GC_REFCOUNT(current->ref);
current = current->next;
} /* Call destructors */
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);
} /* Remove values captured in destructors */
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);
}
} /* Destroy zvals */
GC_TRACE("Destroying zvals");
GC_G(gc_active) = ;
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);
} /* Free objects */
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));
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) = ;
} return count;
}

zend_gc_collect_cycles

参考资料:

PHP7内核剖析

作  者:
Jackson0714

出  处:http://www.cnblogs.com/jackson0714/

关于作者:专注于微软平台的项目开发。如有问题或建议,请多多赐教!

版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信

声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是作者坚持原创和持续写作的最大动力!