对垃圾回收的支持
前面的内容有提到,PHP 7 中复杂类型的引用计数都维护在各个结构体头部的 gc
中,那么 gc
的作用是什么?答案是对垃圾回收的支持。什么是垃圾回收呢?垃圾回收是一种自动的内存管理机制,当一个变量在程序中不再被需要时,应该予以释放,这种内存资源管理称为垃圾回收。其中一种垃圾回收的方式是使用引用计数,通过对数据存储的物理空间多附加一个计数器空间,当其他数据与其相关时,计数器加一,反之,相关解除时计数器减一。定期检查各存储对象的计数器,计数器为零的话,则认为该对象已经被抛弃而应将其所占物理空间回收。
PHP 7 中垃圾回收的实现方法是定期遍历和标记若干存储对象的数组,再通过算法将是垃圾的物理空间回收。那么 PHP 中变量是怎么设计支持垃圾回收的呢?首先来了解一下 gc
的基本结构。
gc 的基本结构
前面的内容有提到,PHP 7 的复杂类型,像字符串、数组、引用等的数据结构中,头部都有一个 gc
,变量的引用计数维护在这个 gc
中,gc
的核心数据结构如下面所示:
typedef struct _zend_refcounted_h {
uint32_t refcount; /* 32bit长度的引用计数 */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /*当前元素的数据类型*/
zend_uchar flags, /* 标记字符串或者对象*/
uint16_t gc_info) /* 记录所在gc池中的位置和颜色 */
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
从代码中可以看出,zend_refcounted
是由 uint32_t
的 refcount
和 uint32_t
的 type_info
组成的,总大小为 8 字节。type_info
中的 4 字节(每个字节 8bit)有着各自的意义,分别如下。
-
type
:第一个字节记录当前元素的类型,同zval
的u1.v.type
。这里为什么要冗余一份呢?3.4.4 节分析过垃圾回收的原理后,你就会明白。 -
flags
:第二个字节用来标记数据类型,可以是字符串类型或数组类型等。其中标记字符串的
flags
有:/* string flags (zval.value->gc.u.flags) */ #define IS_STR_PERSISTENT (1<<0) /* allocated using malloc */ #define IS_STR_INTERNED (1<<1) /* interned string */ #define IS_STR_PERMANENT (1<<2) /* relives request boundary */ #define IS_STR_CONSTANT (1<<3) /* constant index */ #define IS_STR_CONSTANT_UNQUALIFIED (1<<4) /* the same as IS_CONSTANT_UNQUALIFIED */
标记数组的
flags
有:/* array flags */ #define IS_ARRAY_IMMUTABLE (1<<1) /* the same as IS_TYPE_IMMUTABLE */
标记对象的
flags
有:/* object flags (zval.value->gc.u.flags) */ #define IS_OBJ_APPLY_COUNT 0x07 #define IS_OBJ_DESTRUCTOR_CALLED (1<<3) #define IS_OBJ_FREE_CALLED (1<<4) #define IS_OBJ_USE_GUARDS (1<<5) #define IS_OBJ_HAS_GUARDS (1<<6)
-
gc_info
:后面的两个字节标记当前元素的颜色和垃圾回收池中的位置,其中高地址的两位用来标记颜色。#define GC_COLOR 0xc000 /*颜色标志位*/ #define GC_BLACK 0x0000 /*黑色*/ #define GC_WHITE 0x8000 /*白色*/ #define GC_GREY 0x4000 /*灰色*/ #define GC_PURPLE 0xc000 /*紫色*/
根据上面的字段说明,可以画出 zend_refcounted_h
的结构,如图3-12所示。

上段代码中的那些颜色是用来做什么的呢?在了解颜色的功能之前,先来了解一下引用计数。
引用计数
3.2.4 节提到过 “写时拷贝”,这里以 PHP 代码中的字符串为例:
<?php
$i = 0;
$a = "hello".$i;
$b = $a;
unset($a);
unset($b);
在执行完 3 次 assign
操作(具体在第 11 章中详细阐述)后,通过 gdb
打印出 $a
和 $b
对应的 zend_string
结构体的信息,如下面所示:
(gdb) p *executor_globals.symbol_table.arData[8].val.value.zv.value.str
$1 = {gc = {refcount = 2, u = {v = {type = 6 '\006', flags = 0 '\000', gc_info = 0}, type_info = 6}}, h = 0, len = 6, val = "h"}
可以看到 “hello0”
对应的 zend_string.gc.refcount=2
,引用计数是 2,怎么理解呢?如图3-13所示。

从图中可以看出,$b=$a
并没有进行内存的拷贝,而是指向了同一个 zend_string
结构体,而这个 zend_string
结构体的 refcount=2
,此时如果进行 unset($a)
,引用计数减 1:
(gdb) p *executor_globals.symbol_table.arData[9].val.value.zv.value.str
$12 = {gc = {refcount = 1, u = {v = {type = 6 '\006', flags = 0 '\000', gc_info = 0}, type_info = 6}}, h = 0, len = 6, val = "h"}
继续 unset($b)
操作,它的引用计数减到 0:
(gdb)
42774 if (! --GC_REFCOUNT(garbage)) {
(gdb)
42775 ZVAL_UNDEF(var);
(gdb) p *executor_globals.symbol_table.arData[9].val.value.zv.value.str
$13 = {gc = {refcount = 0, u = {v = {type = 6 '\006', flags = 0 '\000', gc_info = 0}, type_info = 6}}, h = 0, len = 6, val = "h"}
发现引用计数为 0,会调用 ZVAL_UNDEF(var)
,将其置为 IS_UNDEF
,继续 gdb
打印出它的信息,如下所示:
(gdb) p *executor_globals.symbol_table.arData[9].val.value.zv
$15 = {value = {lval = 140737350338208, dval = 6.9533489888832513e-310,counted = 0x7ffff7c606a0, str = 0x7ffff7c606a0, arr = 0x7ffff7c606a0,obj = 0x7ffff7c606a0, res = 0x7ffff7c606a0, ref = 0x7ffff7c606a0,ast = 0x7ffff7c606a0, zv = 0x7ffff7c606a0, ptr = 0x7ffff7c606a0,ce = 0x7ffff7c606a0, func = 0x7ffff7c606a0, ww = {w1 = 4156950176,w2 = 32767}}, u1 = {v = {type = 0 '\000', type_flags = 0 '\000',const_flags = 0 '\000', reserved = 0 '\000'}, type_info = 0}, u2 = {next = 0, cache_slot = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0}}
可以看出,此时 u1.v.type=0(IS_UNDEF)
,意味着该变量没有其他地方引用了,那么引用计数有没有问题呢?答案是肯定的,比如有可能存在循环引用问题,下面展开阐述一下。
循环引用问题
以数组为例,在 PHP 7 中使用 “&”
会改变等号两边 zval
的类型(改为 IS_REFERENCE
),引用计数记录在新的结构体(zend_reference
)中,并且引用计数为 2。这时如果等号两边是同一变量,那么这个变量的引用计数就变为 2,自己引用自己。举个例子:
<?php
$a = [];
$a[] = &$a;
unset($a);
如图3-14所示。

当执行 unset
操作后,图3-14中 $a
所在的 zval
类型被标记为 IS_UNDEF
,zend_reference
结构体的引用计数减 1,但仍然大于 0,这时候,后面的结构可能会成为垃圾,如图3-15所示,对此不处理可能会造成内存泄露。垃圾收集器会将这部分可能是垃圾的数据收集到缓冲区,同时加入到 root
环。

垃圾回收
PHP 7 垃圾回收实际包含两部分,垃圾收集器 和 垃圾回收算法。垃圾收集器是将可能是垃圾的元素收集在回收池中,然后由垃圾回收算法回收。
_zend_gc_globals
是 PHP 引擎中的一个结构体,用于管理与垃圾回收相关的全局变量和配置。垃圾回收(Garbage Collection,GC)是一种内存管理技术,用于自动回收不再使用的内存,防止内存泄漏和提高内存利用率。下面看看收集器的核心数据结构:
typedef struct _zend_gc_globals {
zend_bool gc_enabled;
zend_bool gc_active;
zend_bool gc_full;
gc_root_buffer *buf; /*预先分配的缓冲区数组 */
gc_root_buffer roots; /*指向缓冲区中最新加入的可能是垃圾的元素*/
gc_root_buffer *unused; /* 未使用的缓冲区列表 */
gc_root_buffer *first_unused; /* 指向第一个未使用的缓冲区 */
gc_root_buffer *last_unused; /*指向最后一个未使用的缓冲区 */
gc_root_buffer to_free; /* 待释放的列表 */
gc_root_buffer *next_to_free; /*下一个待释放的列表*/
uint32_t gc_runs;
uint32_t collected;
} zend_gc_globals;
typedef struct _gc_root_buffer {
zend_refcounted *ref;
struct _gc_root_buffer *next;
struct _gc_root_buffer *prev;
uint32_t refcount;
} gc_root_buffer; /*双向链表*/
gc_root_buffer
是一个双向链表,同时记录引用计数的相关信息,zend_gc_globals
维护着 gc
的整个信息,各字段含义如下。
-
gc_enabled
:是否开启gc
。 -
gc_active
:垃圾回收算法是否运行。 -
gc_full
:垃圾缓冲区是否满了,在debug
模式下有用。 -
buf
:垃圾缓冲区,PHP 7 默认大小为 10000 个节点位置,第 0 个位置保留,即不会使用,定义在zend/zend_gc.c
文件中。#define GC_ROOT_BUFFER_MAX_ENTRIES 10001
-
roots
:指向缓冲区中最新加入的可能是垃圾的元素。 -
unused
:指向缓冲区中没有使用的位置,在没有启动垃圾回收算法前,指向空。 -
first_unused
:指向缓冲区中第一个未使用的位置,新的元素插入缓冲区后,指针会向后移动一位。 -
last_unused
:指向缓冲区中最后一个位置。 -
to_free
:待释放的列表。 -
next_to_free
:下一个待释放的列表 -
gc_runs
:记录gc
算法运行的次数,当缓冲区满了,才会运行gc
算法。 -
collected
:记录gc
算法回收的垃圾数。
PHP 7 中垃圾回收维护了一个全局变量 gc_globals
的 HashTable
,存取值的宏为 GC_G(v)
。下面来看一下 gc_globals
的结构,如图3-16所示。

|
从图3-16中可以看出,gc_globals
总大小为 120 字节。接下来看一下垃圾回收是如何基于这个结构展开的。
首先说一下 gc
初始化,其代码如下:
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_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
gc_reset();
}
}
首先向操作系统申请 10001 个 gc_root_buffer
结构体大小的内存,然后将 GC_G(buf)
指向首地址,GC_G(last_unused)
指向尾地址,初始化后的结果如图3-17所示。

|
看完初始化后的状态,接着来构造一段 PHP 代码,看一下整个 gc
的调用过程:
<?php
for($i = 0; $i <= 10002; $i++){
$a[$i] = array($i."_string");
$b[] = $a[$i];
unset($a[$i]);
}
在 gc_possible_root
处加断点,查看一下传入的 zend_refcounted
:
(gdb) p ref
$1 = (zend_refcounted *) 0x7ffff7c56230
(gdb) p *ref
$2 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 0}, type_info = 7}}}
首先,看到 ref.gc.refcount
是 1,也就是 unset
操作后引用计数大于 0 才会进入 gc
的列表中。
其次,因为 ref
是在 zend_array
和 zend_object
的头部,所以 ref
的地址跟对应的 zend_array
和 zend_object
的地址是一样的,所以,对于 ref
里面的 u.v.type
冗余是有设计考虑的,可以通过 ref.u.v.type
获取到对应的数据类型。像上面的示例,这样获得了数据类型的首地址,且 u.v.type
等于 7(IS_ARRAY
),因此 ref
对应的数据类型是数组,强转看一下:
(gdb) p *(zend_array*)ref
$3 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 0}, type_info = 7}}, u = {v = {flags = 30 '\036', nApplyCount = 0 '\000', nIteratorsCount = 0 '\000', consistency = 0 '\000'}, flags = 30}, nTableMask = 4294967294, arData = 0x7ffff7c5d008, nNumUsed = 1, nNumOfElements = 1, nTableSize = 8, nInternalPointer = 0, nNextFreeElement = 1, pDestructor = 0x8caf3a <_zval_ptr_dtor>}
可以看出,这个数组变量有一个元素(nNumOfElements=1
),这部分会在后面第 5 章详细阐述。然后来看一下对应的内容:
(gdb) p *((zend_array*)ref).arData[0].val.value.str.val@8
$4 = "0_string"
当有新的可能是垃圾的元素被记录,元素被插入到缓冲区的第一个位置,同时 roots
指向第一个位置,first_unused
向后移动一个 gc_root_buffer
的大小,其变化如图 3-18 所示,roots
指向缓冲区中最新插入的可能是垃圾的元素位置。

从图中可以看出,在进行 unset
操作的时候,如果当前的 refcount
大于 0 会插入到缓冲区的第 1 个位置,其中第 0 个位置是不用的,只用后面 1~10000 个位置,一共 10000 个位置。对于 PHP 代码中的 unset
,因为 $a[0]
赋值给了 $b[0]
,前文提到过 “写时拷贝”,那么在进行 unset
操作前并不会拷贝,而是引用计数加 1。因此当执行 unset
操作后,$a[0]
引用计数依然大于 0, $a[0]
的首地址会插入到缓冲区的 offset=1 位置(赋值给 ref
)。同样,$a[1]
的首地址会插入到缓冲区的 offset=2 位置。同时与 roots
建立一个双向链表,由于图3-19中双向链表的表示有些繁杂,那么抽出来重新表达下。

如果 10000 个节点被插满了呢?PHP 代码中设计插入达到 10000 以上时,可在下面代码中增加断点:
ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
{
//…代码省略…//
282 newRoot = GC_G(unused);
283 if (newRoot) {
284 GC_G(unused) = newRoot->prev;
285 } else if (GC_G(first_unused) ! = GC_G(last_unused)) {
286 newRoot = GC_G(first_unused);
287 GC_G(first_unused)++;
288 } else {
289 if (! GC_G(gc_enabled)) {
290 return;
300 }
可以在 289 行增加断点,一步一步来看一下垃圾回收的过程:
(gdb) b zend_gc.c:289
这里给大家总结了一下垃圾收集的过程:
-
要求数据类型是数组和对象;
-
没有在缓冲区中存在过;
-
没有被标记过;
-
将其
gc_info
标记为紫色,且记录其在缓冲区的位置。
当缓冲区满了,再收集到新的元素就会触发垃圾回收算法。参照图3-18,不难想象,为了将右边的独立元素回收该如何实现这个算法。引用计数大于 0 说明它还在其他地方使用,那么先将元素的引用计数减 1。如果发现引用计数为 0,则说明任何地方都不再使用它,那么它就是垃圾,需要被回收掉。反之说明不是垃圾,需要将其从回收池移出去。而垃圾回收算法也是围绕这个核心条件进行的。
这段代码通过一系列检查和操作,确保垃圾回收机制能够有效地处理和跟踪新的根对象,从而优化内存管理和性能。 |
PHP7 垃圾回收算法在 zend/zend_gc.c
文件中的 zend_gc_collect_cycles
函数,该函数是 gc_collect_cycles
函数的实现,代码如下:
ZEND_API int zend_gc_collect_cycles(void)
{
if (GC_G(roots).next ! = &GC_G(roots)) {
...
gc_mark_roots(); /*标记阶段,扫描roots,将紫色标记为灰色*/
...
gc_scan_roots(); /*扫描roots,将灰色标记为白色*/
...
/*扫描roots,收集白色的元素 并将黑色元素移出roots*/
count = gc_collect_roots(&gc_flags, &additional_buffer);
...
/*如果对象定义了自己的析构函数*/
if (gc_flags & GC_HAS_DESTRUCTORS) {
...
if (EG(objects_store).object_buckets) {
...
while (current ! = &to_free) {
...
if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_OBJECT) {
...
obj->handlers->dtor_obj(obj);
...
}
current = GC_G(next_to_free);
}
...
}
}
/*对象调用默认的析构函数,数组用HashTable的释放方式*/
while (current ! = &to_free) {
...
if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_OBJECT) {
...
if (EG(objects_store).object_buckets &&
IS_OBJ_VALID(EG(objects_store).object_buckets[obj->handle]))
{
EG(objects_store).object_buckets[obj->handle] = SET_OBJ_
INVALID(obj);
...
if (obj->handlers->free_obj) {
...
obj->handlers->free_obj(obj);
...
}
}
...
}
} else if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_ARRAY) {
...
zend_hash_destroy(arr);
}
current = GC_G(next_to_free);
}
...
}
回收的过程大致可以分为4步,如图3-20所示。

-
对
roots
环中每个元素进行深度优先遍历,将每个元素中gc_info
为紫色的标记元素为灰色,且引用计数减 1。 -
扫描
roots
环中gc_info
为灰色的元素,如果发现其引用计数仍旧大于 0,说明这个元素还在其他地方使用,那么将其颜色重新标记会黑色,并将其引用计数加 1(在第一步有减 1 操作,需要恢复该值)。如果发现其引用计数为 0,则将其标记为白色。该过程同样为深度优先遍历。 -
扫描
roots
环,将gc_info
颜色为黑色的元素从roots
移除。然后对roots
中颜色为白色的元素进行深度优先遍历,将其引用计数加 1(在第一步有减 1 操作,需要恢复该值),然后将roots
链表移动到待释放的列表中(to_free
)。 -
释放
to_free
列表的元素。
在上面的流程中比较耗费时间的是对数组或者对象的深度优先遍历,但是对对象的遍历与对数组的遍历最大的不同是对象有两个属性表。对象是类的实例,有继承类的默认属性表 default_properties_table
,但同时类支持动态属性,所以也有自己的 properties_table
(后面的章节会有详细讲解,这里暂不展开)。在对类成员深度优先遍历时会将两个表进行重建合并(最终调用 rebuild_object_properties
),调用函数名为 zend_std_get_gc
,该函数维护在 std_object_handlers
中,在类初始化时会赋值对象的 handler
。
ZEND_API zend_object_handlers std_object_handlers = {
...
zend_object_std_dtor, /* free_obj */
zend_objects_destroy_object, /* dtor_obj */
...
zend_std_get_gc, /* get_gc */
...
};
gc
算法在对类进行释放时默认会调用 zend_object_std_dtor
函数,如果有定义 dtor_obj
析构函数,会优先调用定义的析构函数。
总结
在 PHP 7 中,gc_possible_root
是垃圾回收(Garbage Collection,GC)机制中的一个重要概念,用于处理可能需要进行垃圾回收的对象(即可能的根对象)。为了更好地理解 gc_possible_root
的作用,我们需要深入了解 PHP 7 的垃圾回收机制。
PHP 7 垃圾回收机制概述
PHP 7 的垃圾回收机制主要依赖引用计数来管理内存,但也包含了处理循环引用的 标记-清除算法。引用计数简单有效,但无法处理循环引用问题。为了解决这个问题,PHP 7 引入了根缓冲区(Root Buffer)和可能的根对象(Possible Roots)来追踪和管理可能参与循环引用的对象。
gc_possible_root 的定义
gc_possible_root 是用来标记那些需要进一步检查的对象,这些对象可能是根对象的一部分,在下一次垃圾回收过程中需要进行标记和清除。
gc_possible_root 的作用
-
标记可能的根对象:
-
当一个对象的引用计数递减时,如果其引用计数仍然大于零,则可能参与循环引用。这时,PHP 会将这个对象标记为 gc_possible_root,并将其添加到根缓冲区中。
-
-
管理根缓冲区:
-
根缓冲区(Root Buffer)用于存储所有可能的根对象。在垃圾回收的标记阶段,PHP 会遍历根缓冲区中的所有对象,进行标记处理。根缓冲区的管理包括添加、删除和重置根对象。
-
-
标记-清除算法的触发:
-
当根缓冲区中的对象数量达到一定阈值时,PHP 会触发标记-清除算法,遍历所有 gc_possible_root 对象进行标记和清除。这种方式有效地处理了引用计数无法解决的循环引用问题。
-
gc_possible_root 是 PHP 7 垃圾回收机制中的一个关键概念和工具,用于标记和管理可能需要进行垃圾回收的对象。通过将可能的根对象添加到根缓冲区,PHP 7 的垃圾回收器能够有效地处理循环引用问题,确保内存的正确管理和释放。
标记清除算法的引入是 PHP 5.3 中一个重要的改进,增强了 PHP 的垃圾回收机制,使其能够更有效地管理内存,尤其是处理循环引用的问题。
在 PHP 7 中,垃圾回收(Garbage Collection,GC)机制依然使用标记清除算法来处理内存管理问题,特别是解决循环引用带来的内存泄漏问题。虽然 PHP 7 对整体的内存管理和性能进行了诸多改进,但标记清除算法作为垃圾回收的重要组成部分仍然被保留并优化。
PHP 7 对垃圾回收机制进行了多项优化,包括:
-
更高效的引用计数处理:PHP 7 中的
zval
结构体进行了重新设计,减小了内存占用,并优化了引用计数的操作。 -
改进的根缓冲区管理:PHP 7 的根缓冲区管理更加高效,减少了不必要的内存分配和释放操作。
-
分代垃圾回收:虽然 PHP 7 主要依赖引用计数和标记清除算法,但它也引入了一些分代垃圾回收的概念,对短命和长命对象采用不同的处理策略,提高了垃圾回收的效率。
|
垃圾回收的触发条件
PHP 中的垃圾回收不会在每次变量释放时都进行。相反,垃圾回收会在以下条件下触发:
-
达到特定阈值:PHP 会维护一个根缓冲区(root buffer),用于存储可能参与循环引用的对象。当根缓冲区中的对象数量达到特定阈值时,会触发垃圾回收过程。
在 PHP 7 中,这个阈值默认为 10,000。可以通过 gc_collect_cycles() 函数手动触发垃圾回收过程。
-
手动触发:开发者可以使用 gc_collect_cycles() 函数手动触发垃圾回收过程。这在某些情况下非常有用,例如在执行一段占用大量内存的代码之后,可以显式地进行垃圾回收以释放内存。
-
自动触发:当根缓冲区中的对象数量达到设定的阈值时,PHP 会自动触发垃圾回收过程。这是 PHP 内存管理的一部分,旨在确保不会因循环引用导致内存泄漏。
PHP 的垃圾回收主要依赖于 引用计数 和 标记-清除算法。当变量的引用计数降为零时,内存会立即释放。为了处理循环引用问题,PHP 会在特定条件下(例如达到根缓冲区的阈值或手动触发)执行标记-清除算法进行垃圾回收。通过这些机制,PHP 能够有效地管理内存,防止内存泄漏。
对象放入根缓冲区的时机
在 PHP 的垃圾回收(GC)机制中,将对象放入根缓冲区的时机和过程如下:
-
对象创建时
当 PHP 创建新的对象时,它们可能会被立即放入根缓冲区。这通常发生在以下情况下:
-
全局变量:全局变量是程序的根对象,因此它们在程序启动时会被放入根缓冲区。
-
静态变量:静态变量在函数或方法中使用,也会被放入根缓冲区。
-
注册对象:某些扩展或用户代码可能会注册对象,使其成为根对象。
-
-
对象的引用被增加
当对象的引用计数增加时(例如,将对象赋值给新的变量),该对象可能会被添加到根缓冲区,以防止它在垃圾回收期间被错误地释放。尤其是在对象被用作全局变量或静态变量时,这种情况非常常见。
-
标记阶段
在垃圾回收的标记阶段,根缓冲区中的对象被标记为根对象。根缓冲区提供了垃圾回收系统可以使用的稳定的起点,这些对象作为垃圾回收的根节点开始被扫描。以下是一些具体情况:
-
根对象标记:所有根对象(如全局变量、静态变量等)在标记阶段会被扫描,并从根缓冲区开始标记。
-
栈对象:函数调用的局部变量对象也可能被标记为根对象。
-
-
对象从根缓冲区移除
当对象被从根缓冲区中移除时,通常发生在以下情况:
-
对象生命周期结束:例如,全局变量或静态变量的作用域结束,或者它们被显式销毁时。
-
垃圾回收:在垃圾回收的过程中,如果对象被标记为不再需要,它们可能会被从根缓冲区移除。
-
-
处理根缓冲区
根缓冲区的处理过程主要包括:
-
标记根对象:
gc_mark_roots
函数会遍历根缓冲区中的对象,并将它们标记为活动对象。 -
扫描根对象:
gc_scan_roots
函数扫描根缓冲区,找出所有引用的对象。 -
收集根对象:
gc_collect_roots
函数处理根对象,并进行必要的清理。
-
-
额外的情况
某些扩展或底层操作可能会手动将对象放入根缓冲区。例如,在扩展中直接操作对象生命周期或将对象添加到特定的根缓冲区。
对象放入根缓冲区的时机包括对象创建、引用计数增加、标记阶段等。这些操作确保对象在垃圾回收过程中不会被错误地回收,并帮助垃圾回收系统正确地管理对象生命周期。