PHP内核之旅-6.垃圾回收机制
回收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.垃圾回收的步骤
步骤一:遍历垃圾回收器的buffer缓冲区,把value标为灰色,把value的成员的refount-1,标为白色。
步骤二:遍历垃圾回收器的buffer缓冲区,如果value的 refcount等于0,则认为是垃圾,标为白色;如果不等于0,则表示还有外部的引用,不是垃圾,将refcount+1还原回去,标为黑色。
步骤三:遍历垃圾回收器的buffer缓冲区,将value为非白色的节点从buffer中删除,最终buffer缓冲区中都是真正的垃圾。
步骤四:遍历垃圾回收器的buffer缓冲区,释放此value。
三、代码实现
1.垃圾管家
1 typedef struct _zend_gc_globals { 2 zend_bool gc_enabled; //是否启用GC 3 zend_bool gc_active; //是否处于垃圾检查中 4 zend_bool gc_full; //缓存区是否已满 5 6 gc_root_buffer *buf; //预分配的垃圾缓存区,用于保存可能成为垃圾的value 7 gc_root_buffer roots; //指向buf中最新加入的一个可能垃圾 8 gc_root_buffer *unused; //指向buf中没有使用的buffer 9 gc_root_buffer *first_unused; //指向第一个没有使用的buffer 10 gc_root_buffer *last_unused; //指向最后一个没有使用的buffer 11 12 gc_root_buffer to_free; //待释放的垃圾 13 gc_root_buffer *next_to_free; //下指向下一个待释放的垃圾 14 15 uint32_t gc_runs; //统计GC运行次数 16 uint32_t collected; //统计已回收的垃圾数 17 18 #if GC_BENCH 19 uint32_t root_buf_length; 20 uint32_t root_buf_peak; 21 uint32_t zval_possible_root; 22 uint32_t zval_buffered; 23 uint32_t zval_remove_from_buffer; 24 uint32_t zval_marked_grey; 25 #endif 26 27 gc_additional_buffer *additional_buffer; 28 29 } zend_gc_globals;
_zend_gc_globals
2.垃圾管家初始化
(1)php.ini解析后调用gc_init()初始垃圾管家_zend_gc_globals
文件路径:\Zend\zend_gc.c
1 ZEND_API void gc_init(void) 2 { 3 if (GC_G(buf) == NULL && GC_G(gc_enabled)) { 4 GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);//GC_ROOT_BUFFER_MAX_ENTRIES=10001 5 GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES]; 6 gc_reset(); 7 } 8 }
gc_init
(2)gc_init()函数里面调用gc_reset()函数初始化变量
1 ZEND_API void gc_reset(void) 2 { 3 GC_G(gc_runs) = 0; 4 GC_G(collected) = 0; 5 GC_G(gc_full) = 0; 6 7 GC_G(roots).next = &GC_G(roots); 8 GC_G(roots).prev = &GC_G(roots); 9 10 GC_G(to_free).next = &GC_G(to_free); 11 GC_G(to_free).prev = &GC_G(to_free); 12 13 GC_G(unused) = NULL; 14 GC_G(first_unused) = NULL; 15 GC_G(last_unused) = NULL; 16 17 GC_G(additional_buffer) = NULL; 18 }
gc_reset
3.判断是否需要收集
(1)在销毁一个变量时就会判断是否需要收集。调用i_zval_ptr_dtor()函数
文件路径:Zend\zend_variables.h
1 static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC) 2 { 3 if (Z_REFCOUNTED_P(zval_ptr)) {//type_flags & IS_TYPE_REFCOUNTED 4 zend_refcounted *ref = Z_COUNTED_P(zval_ptr); 5 if (!--GC_REFCOUNT(ref)) {//refcount - 1 之后等于0,则不是垃圾,正常回收 6 _zval_dtor_func(ref ZEND_FILE_LINE_RELAY_CC); 7 } else {//如果refcount - 1 之后仍然大于0,垃圾管家进行收集 8 gc_check_possible_root(ref); 9 } 10 } 11 }
i_zval_ptr_dtor
(2)如果refcount减一后,refcount等于0,则认为不是垃圾,释放此value
1 //文件路径:\Zend\zend_variables.c 2 ZEND_API void ZEND_FASTCALL _zval_dtor_func(zend_refcounted *p ZEND_FILE_LINE_DC) 3 { 4 switch (GC_TYPE(p)) { 5 case IS_STRING: 6 case IS_CONSTANT: { 7 zend_string *str = (zend_string*)p; 8 CHECK_ZVAL_STRING_REL(str); 9 zend_string_free(str); 10 break; 11 } 12 case IS_ARRAY: { 13 zend_array *arr = (zend_array*)p; 14 zend_array_destroy(arr); 15 break; 16 } 17 case IS_CONSTANT_AST: { 18 zend_ast_ref *ast = (zend_ast_ref*)p; 19 20 zend_ast_destroy_and_free(ast->ast); 21 efree_size(ast, sizeof(zend_ast_ref)); 22 break; 23 } 24 case IS_OBJECT: { 25 zend_object *obj = (zend_object*)p; 26 27 zend_objects_store_del(obj); 28 break; 29 } 30 case IS_RESOURCE: { 31 zend_resource *res = (zend_resource*)p; 32 33 /* destroy resource */ 34 zend_list_free(res); 35 break; 36 } 37 case IS_REFERENCE: { 38 zend_reference *ref = (zend_reference*)p; 39 40 i_zval_ptr_dtor(&ref->val ZEND_FILE_LINE_RELAY_CC); 41 efree_size(ref, sizeof(zend_reference)); 42 break; 43 } 44 default: 45 break; 46 } 47 }
_zval_dtor_func
(3)如果refcount减一后,refcount大于0,则认为value可能是垃圾,垃圾管家进行收集
1 \\文件路径:\Zend\zend_gc.h 2 static zend_always_inline void gc_check_possible_root(zend_refcounted *ref) 3 { 4 if (GC_TYPE(ref) == IS_REFERENCE) { 5 zval *zv = &((zend_reference*)ref)->val; 6 7 if (!Z_REFCOUNTED_P(zv)) { 8 /* 9 Z_TYPE_FLAGS 与 IS_TYPE_REFCOUNTED 与运算后,不等于0,则会被释放掉 10 Z_REFCOUNTED_P --> ((Z_TYPE_FLAGS(zval) & IS_TYPE_REFCOUNTED) != 0) 11 Z_TYPE_FLAGS(zval) --> (zval).u1.v.type_flags 12 IS_TYPE_REFCOUNTED -> 1<<2 (0100) 13 */ 14 return; 15 } 16 ref = Z_COUNTED_P(zv); //Z_COUNTED_P --> (zval).value.counted GC头部 17 } 18 if (UNEXPECTED(GC_MAY_LEAK(ref))) { 19 gc_possible_root(ref); //垃圾管家收集可能的垃圾 20 } 21 }
gc_check_possible_root
4.收集垃圾
1 \\文件路径:\Zend\zend_gc.c 2 ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref) 3 { 4 gc_root_buffer *newRoot; 5 6 if (UNEXPECTED(CG(unclean_shutdown)) || UNEXPECTED(GC_G(gc_active))) { 7 return; 8 } 9 10 ZEND_ASSERT(GC_TYPE(ref) == IS_ARRAY || GC_TYPE(ref) == IS_OBJECT); // 只有数组和对象才会出现循环引用的产生的垃圾,所以只需要收集数组类型和对象类型的垃圾 11 ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK)); // 只收集颜色为GC_BLACK的变量 12 ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref))); 13 14 GC_BENCH_INC(zval_possible_root); 15 16 newRoot = GC_G(unused); //拿出unused指向的节点 17 if (newRoot) { //如果拿出的节点是可用的,则将unused指向下一个节点 18 GC_G(unused) = newRoot->prev; 19 } else if (GC_G(first_unused) != GC_G(last_unused)) {//如果unused没有可用的,且first_unused还没有推进到last_unused,则表示buf缓存区中还有可用的节点 20 newRoot = GC_G(first_unused); //拿出first_unused指向的节点 21 GC_G(first_unused)++; //first_unused指向下一个节点 22 } else {//buf缓存区已满,启动垃圾鉴定、垃圾回收 23 if (!GC_G(gc_enabled)) { //如果未启用垃圾回收,则直接返回 24 return; 25 } 26 GC_REFCOUNT(ref)++; 27 gc_collect_cycles(); 28 GC_REFCOUNT(ref)--; 29 if (UNEXPECTED(GC_REFCOUNT(ref)) == 0) { 30 zval_dtor_func(ref); 31 return; 32 } 33 if (UNEXPECTED(GC_INFO(ref))) { 34 return; 35 } 36 newRoot = GC_G(unused); 37 if (!newRoot) { 38 #if ZEND_GC_DEBUG 39 if (!GC_G(gc_full)) { 40 fprintf(stderr, "GC: no space to record new root candidate\n"); 41 GC_G(gc_full) = 1; 42 } 43 #endif 44 return; 45 } 46 GC_G(unused) = newRoot->prev; 47 } 48 49 GC_TRACE_SET_COLOR(ref, GC_PURPLE); //将插入的变量标为紫色,防止重复插入 50 //将该节点在buf数组中的位置保存到了gc_info中,当后续value的refcount变为了0, 51 //需要将其从buf中删除时可以知道该value保存在哪个gc_root_buffer中 52 GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE; 53 newRoot->ref = ref; 54 55 //插入roots链表头部 56 newRoot->next = GC_G(roots).next; 57 newRoot->prev = &GC_G(roots); 58 GC_G(roots).next->prev = newRoot; 59 GC_G(roots).next = newRoot; 60 61 GC_BENCH_INC(zval_buffered); 62 GC_BENCH_INC(root_buf_length); 63 GC_BENCH_PEAK(root_buf_peak, root_buf_length); 64 }
gc_possible_root
5.释放垃圾
1 ZEND_API int zend_gc_collect_cycles(void) 2 { 3 int count = 0; 4 5 if (GC_G(roots).next != &GC_G(roots)) { 6 gc_root_buffer *current, *next, *orig_next_to_free; 7 zend_refcounted *p; 8 gc_root_buffer to_free; 9 uint32_t gc_flags = 0; 10 gc_additional_buffer *additional_buffer_snapshot; 11 #if ZEND_GC_DEBUG 12 zend_bool orig_gc_full; 13 #endif 14 15 if (GC_G(gc_active)) { 16 return 0; 17 } 18 19 GC_TRACE("Collecting cycles"); 20 GC_G(gc_runs)++; 21 GC_G(gc_active) = 1; 22 23 GC_TRACE("Marking roots"); 24 gc_mark_roots(); 25 GC_TRACE("Scanning roots"); 26 gc_scan_roots(); 27 28 #if ZEND_GC_DEBUG 29 orig_gc_full = GC_G(gc_full); 30 GC_G(gc_full) = 0; 31 #endif 32 33 GC_TRACE("Collecting roots"); 34 additional_buffer_snapshot = GC_G(additional_buffer); 35 count = gc_collect_roots(&gc_flags); 36 #if ZEND_GC_DEBUG 37 GC_G(gc_full) = orig_gc_full; 38 #endif 39 GC_G(gc_active) = 0; 40 41 if (GC_G(to_free).next == &GC_G(to_free)) { 42 /* nothing to free */ 43 GC_TRACE("Nothing to free"); 44 return 0; 45 } 46 47 /* Copy global to_free list into local list */ 48 to_free.next = GC_G(to_free).next; 49 to_free.prev = GC_G(to_free).prev; 50 to_free.next->prev = &to_free; 51 to_free.prev->next = &to_free; 52 53 /* Free global list */ 54 GC_G(to_free).next = &GC_G(to_free); 55 GC_G(to_free).prev = &GC_G(to_free); 56 57 orig_next_to_free = GC_G(next_to_free); 58 59 #if ZEND_GC_DEBUG 60 orig_gc_full = GC_G(gc_full); 61 GC_G(gc_full) = 0; 62 #endif 63 64 if (gc_flags & GC_HAS_DESTRUCTORS) { 65 GC_TRACE("Calling destructors"); 66 67 /* Remember reference counters before calling destructors */ 68 current = to_free.next; 69 while (current != &to_free) { 70 current->refcount = GC_REFCOUNT(current->ref); 71 current = current->next; 72 } 73 74 /* Call destructors */ 75 current = to_free.next; 76 while (current != &to_free) { 77 p = current->ref; 78 GC_G(next_to_free) = current->next; 79 if (GC_TYPE(p) == IS_OBJECT) { 80 zend_object *obj = (zend_object*)p; 81 82 if (!(GC_FLAGS(obj) & IS_OBJ_DESTRUCTOR_CALLED)) { 83 GC_TRACE_REF(obj, "calling destructor"); 84 GC_FLAGS(obj) |= IS_OBJ_DESTRUCTOR_CALLED; 85 if (obj->handlers->dtor_obj 86 && (obj->handlers->dtor_obj != zend_objects_destroy_object 87 || obj->ce->destructor)) { 88 GC_REFCOUNT(obj)++; 89 obj->handlers->dtor_obj(obj); 90 GC_REFCOUNT(obj)--; 91 } 92 } 93 } 94 current = GC_G(next_to_free); 95 } 96 97 /* Remove values captured in destructors */ 98 current = to_free.next; 99 while (current != &to_free) { 100 GC_G(next_to_free) = current->next; 101 if (GC_REFCOUNT(current->ref) > current->refcount) { 102 gc_remove_nested_data_from_buffer(current->ref, current); 103 } 104 current = GC_G(next_to_free); 105 } 106 } 107 108 /* Destroy zvals */ 109 GC_TRACE("Destroying zvals"); 110 GC_G(gc_active) = 1; 111 current = to_free.next; 112 while (current != &to_free) { 113 p = current->ref; 114 GC_G(next_to_free) = current->next; 115 GC_TRACE_REF(p, "destroying"); 116 if (GC_TYPE(p) == IS_OBJECT) { 117 zend_object *obj = (zend_object*)p; 118 119 EG(objects_store).object_buckets[obj->handle] = SET_OBJ_INVALID(obj); 120 GC_TYPE(obj) = IS_NULL; 121 if (!(GC_FLAGS(obj) & IS_OBJ_FREE_CALLED)) { 122 GC_FLAGS(obj) |= IS_OBJ_FREE_CALLED; 123 if (obj->handlers->free_obj) { 124 GC_REFCOUNT(obj)++; 125 obj->handlers->free_obj(obj); 126 GC_REFCOUNT(obj)--; 127 } 128 } 129 SET_OBJ_BUCKET_NUMBER(EG(objects_store).object_buckets[obj->handle], EG(objects_store).free_list_head); 130 EG(objects_store).free_list_head = obj->handle; 131 p = current->ref = (zend_refcounted*)(((char*)obj) - obj->handlers->offset); 132 } else if (GC_TYPE(p) == IS_ARRAY) { 133 zend_array *arr = (zend_array*)p; 134 135 GC_TYPE(arr) = IS_NULL; 136 137 /* GC may destroy arrays with rc>1. This is valid and safe. */ 138 HT_ALLOW_COW_VIOLATION(arr); 139 140 zend_hash_destroy(arr); 141 } 142 current = GC_G(next_to_free); 143 } 144 145 /* Free objects */ 146 current = to_free.next; 147 while (current != &to_free) { 148 next = current->next; 149 p = current->ref; 150 if (EXPECTED(current >= GC_G(buf) && current < GC_G(buf) + GC_ROOT_BUFFER_MAX_ENTRIES)) { 151 current->prev = GC_G(unused); 152 GC_G(unused) = current; 153 } 154 efree(p); 155 current = next; 156 } 157 158 while (GC_G(additional_buffer) != additional_buffer_snapshot) { 159 gc_additional_buffer *next = GC_G(additional_buffer)->next; 160 efree(GC_G(additional_buffer)); 161 GC_G(additional_buffer) = next; 162 } 163 164 GC_TRACE("Collection finished"); 165 GC_G(collected) += count; 166 GC_G(next_to_free) = orig_next_to_free; 167 #if ZEND_GC_DEBUG 168 GC_G(gc_full) = orig_gc_full; 169 #endif 170 GC_G(gc_active) = 0; 171 } 172 173 return count; 174 }
zend_gc_collect_cycles
参考资料:
PHP7内核剖析