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

一、引用计数

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

当有新的变量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.垃圾管家

_zend_gc_globals 对垃圾进行管理,收集到的可能成为垃圾的value就保存在这个结构的buf中,称为垃圾缓存区。

文件路劲:\Zend\zend_gc.h

 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;

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 }

(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 }

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 }

(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 }

(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 }

 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 }

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 }

参考资料:

PHP7内核剖析

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java达人

Oracle执行计划详解

简介: 本文全面详细介绍oracle执行计划的相关的概念,访问数据的存取方法,表之间的连接等内容。 并有总结和概述,便于理解与记忆! +++ 目录 ...

1707
来自专栏Hadoop数据仓库

HAWQ技术解析(十二) —— 查询优化

        即便对SELECT等数据库查询语句已经很熟悉了,但HAWQ里的查询有其自己的特点,还是需要研究一下。 一、HAWQ的查询处理流程        ...

3976
来自专栏数说戏聊

07-08 创建计算字段使用函数处理数据第7章 创建计算字段第8章 使用函数处理数据

上述例子中,存储在表中的数据都不是应用程序所需要的。我们需要直接从数据库中检索出转换、计算或格式化过的数据,而不是检索出数据,然后再在客户端应用程序中重新格式化...

812
来自专栏杨建荣的学习笔记

有趣的rownum测试(r10笔记第49天)

rownum在平时的使用中总是一个很自然的语法。如果说这个rownum是否有规律,可能很多人都会模棱两可。到底是还是不是呢,我们来做几个测试来说明。 这个结果也...

32212
来自专栏Kevin-ZhangCG

数据库索引

索引就是加快检索表中数据的方法。数据库的索引类似于书籍的索引。在书籍中,索引允许用户不必翻阅完整个书就能迅速地找到所需要的信息。在数据库中,索引也允许数据库...

400
来自专栏沃趣科技

ASM 翻译系列第三十九弹:物理元数据AT表

原作者:Bane Radulovic 译者: 魏兴华 审核: 魏兴华 DBGeeK联合出品 原文链接:http://asmsupportguy.bl...

2717
来自专栏Spark学习技巧

Phoenix边讲架构边调优

一 基础架构详解 1 概念 讲调优之前,需要大家深入了解phoenix的架构,这样才能更好的调优。 Apache Phoenix在Hadoop中实现OLTP和...

7328
来自专栏一个爱瞎折腾的程序猿

sqlserver使用存储过程跟踪SQL

USE [master] GO /****** Object: StoredProcedure [dbo].[sp_perfworkload_trace_s...

680
来自专栏沃趣科技

利用sys schema解决一次诡异的语句hang问题

一、故事背景 在开始之前,先列出数据库的运行环境信息 操作系统:redhat 7.2 x8_64 文件系统:xfs 数据库版本:MySQL 5.7.17 主机配...

3375
来自专栏java达人

Oracle执行计划详解

简介: 本文全面详细介绍oracle执行计划的相关的概念,访问数据的存取方法,表之间的连接等内容。 并有总结和概述,便于理解与记忆! +++ 目录 ...

24310

扫码关注云+社区