对垃圾回收的支持

前面的内容有提到,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_trefcountuint32_ttype_info 组成的,总大小为 8 字节。type_info 中的 4 字节(每个字节 8bit)有着各自的意义,分别如下。

  1. type:第一个字节记录当前元素的类型,同 zvalu1.v.type。这里为什么要冗余一份呢?3.4.4 节分析过垃圾回收的原理后,你就会明白。

  2. 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)
  3. 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所示。

image 2024 06 07 18 30 31 816
Figure 1. 图3-12 zend_refcounted_h的结构

上段代码中的那些颜色是用来做什么的呢?在了解颜色的功能之前,先来了解一下引用计数。

引用计数

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所示。

image 2024 06 07 18 47 27 301
Figure 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所示。

image 2024 06 07 18 54 04 473
Figure 3. 图3-14 unset($a)操作之前

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

image 2024 06 07 18 55 25 379
Figure 4. 图3-15 unset($a)操作之后

垃圾回收

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 的整个信息,各字段含义如下。

  1. gc_enabled:是否开启 gc

  2. gc_active:垃圾回收算法是否运行。

  3. gc_full:垃圾缓冲区是否满了,在 debug 模式下有用。

  4. buf:垃圾缓冲区,PHP 7 默认大小为 10000 个节点位置,第 0 个位置保留,即不会使用,定义在 zend/zend_gc.c 文件中。

    #define GC_ROOT_BUFFER_MAX_ENTRIES 10001
  5. roots:指向缓冲区中最新加入的可能是垃圾的元素。

  6. unused:指向缓冲区中没有使用的位置,在没有启动垃圾回收算法前,指向空。

  7. first_unused:指向缓冲区中第一个未使用的位置,新的元素插入缓冲区后,指针会向后移动一位。

  8. last_unused:指向缓冲区中最后一个位置。

  9. to_free:待释放的列表。

  10. next_to_free:下一个待释放的列表

  11. gc_runs:记录 gc 算法运行的次数,当缓冲区满了,才会运行 gc 算法。

  12. collected:记录 gc 算法回收的垃圾数。

roots 和 to_free 成员区别

roots 是一个指向垃圾回收根节点的指针。根节点是垃圾回收器开始追踪内存块的起点。在垃圾回收过程中,所有可以直接访问的变量和对象都会被视为根节点。垃圾回收器通过根节点开始遍历整个对象图,以标记和清除不再使用的内存。

  • 作用: 追踪和管理所有根节点,以便在垃圾回收过程中从这些节点开始遍历和标记。

  • 用法: 每当一个新的根节点被分配时,它会被添加到 roots 链表中。垃圾回收器会从 roots 开始遍历,标记所有可以访问的对象。

to_free 是一个指向需要释放内存的对象的指针列表。当垃圾回收器确定某些对象不再需要时,这些对象会被加入 to_free 列表,等待被实际释放。

  • 作用: 存储待释放的对象,以便在适当的时间点释放其内存。

  • 用法: 在垃圾回收的标记阶段后,确定某些对象不再被引用,这些对象会被加入 to_free 列表。在垃圾回收的清除阶段,to_free 列表中的对象将被遍历并释放内存。

PHP 7 中垃圾回收维护了一个全局变量 gc_globalsHashTable,存取值的宏为 GC_G(v)。下面来看一下 gc_globals 的结构,如图3-16所示。

image 2024 06 07 18 59 27 192
Figure 5. 图3-16 gc_globals的结构

to_free 应该去掉指针。

从图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所示。

image 2024 06 07 19 01 35 977
Figure 6. 图3-17 gc初始化结果

unused 错误,应该是 buf 指向 0 地址。

看完初始化后的状态,接着来构造一段 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_arrayzend_object 的头部,所以 ref 的地址跟对应的 zend_arrayzend_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 指向缓冲区中最新插入的可能是垃圾的元素位置。

image 2024 06 07 19 06 48 505
Figure 7. 图3-18 gc算法示意

从图中可以看出,在进行 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中双向链表的表示有些繁杂,那么抽出来重新表达下。

image 2024 06 07 19 08 20 792
Figure 8. 图3-19 gc双向链表示意图

如果 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

这里给大家总结了一下垃圾收集的过程:

  1. 要求数据类型是数组和对象;

  2. 没有在缓冲区中存在过;

  3. 没有被标记过;

  4. 将其 gc_info 标记为紫色,且记录其在缓冲区的位置。

当缓冲区满了,再收集到新的元素就会触发垃圾回收算法。参照图3-18,不难想象,为了将右边的独立元素回收该如何实现这个算法。引用计数大于 0 说明它还在其他地方使用,那么先将元素的引用计数减 1。如果发现引用计数为 0,则说明任何地方都不再使用它,那么它就是垃圾,需要被回收掉。反之说明不是垃圾,需要将其从回收池移出去。而垃圾回收算法也是围绕这个核心条件进行的。

gc_possible_root 函数用于处理新的垃圾回收根对象,并将其插入到根节点链表中。具体步骤如下:

  1. 尝试从未使用的根节点链表或 first_unused 获取新的根节点。

  2. 如果根节点链表已满,调用垃圾回收函数并重新尝试获取根节点。

  3. 设置对象颜色为 GC_PURPLE 并更新附加信息。

  4. 将新根节点插入到根节点链表的开头。

  5. 增加相关计数器,用于性能分析。

这段代码通过一系列检查和操作,确保垃圾回收机制能够有效地处理和跟踪新的根对象,从而优化内存管理和性能。

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所示。

image 2024 06 07 19 12 16 138
Figure 9. 图3-20 垃圾回收算法流转图
  1. roots 环中每个元素进行深度优先遍历,将每个元素中 gc_info 为紫色的标记元素为灰色,且引用计数减 1。

  2. 扫描 roots 环中 gc_info 为灰色的元素,如果发现其引用计数仍旧大于 0,说明这个元素还在其他地方使用,那么将其颜色重新标记会黑色,并将其引用计数加 1(在第一步有减 1 操作,需要恢复该值)。如果发现其引用计数为 0,则将其标记为白色。该过程同样为深度优先遍历。

  3. 扫描 roots 环,将 gc_info 颜色为黑色的元素从 roots 移除。然后对 roots 中颜色为白色的元素进行深度优先遍历,将其引用计数加 1(在第一步有减 1 操作,需要恢复该值),然后将 roots 链表移动到待释放的列表中(to_free)。

  4. 释放 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 的作用

  1. 标记可能的根对象:

    • 当一个对象的引用计数递减时,如果其引用计数仍然大于零,则可能参与循环引用。这时,PHP 会将这个对象标记为 gc_possible_root,并将其添加到根缓冲区中。

  2. 管理根缓冲区:

    • 根缓冲区(Root Buffer)用于存储所有可能的根对象。在垃圾回收的标记阶段,PHP 会遍历根缓冲区中的所有对象,进行标记处理。根缓冲区的管理包括添加、删除和重置根对象。

  3. 标记-清除算法的触发:

    • 当根缓冲区中的对象数量达到一定阈值时,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 5.3:引入标记清除算法,解决引用计数无法处理的循环引用问题。

  • PHP 7:通过重新设计 zval 结构体和优化引用计数操作,提高了内存管理的效率,但并没有实现分代垃圾回收。

垃圾回收的触发条件

PHP 中的垃圾回收不会在每次变量释放时都进行。相反,垃圾回收会在以下条件下触发:

  • 达到特定阈值:PHP 会维护一个根缓冲区(root buffer),用于存储可能参与循环引用的对象。当根缓冲区中的对象数量达到特定阈值时,会触发垃圾回收过程。

    在 PHP 7 中,这个阈值默认为 10,000。可以通过 gc_collect_cycles() 函数手动触发垃圾回收过程。

  • 手动触发:开发者可以使用 gc_collect_cycles() 函数手动触发垃圾回收过程。这在某些情况下非常有用,例如在执行一段占用大量内存的代码之后,可以显式地进行垃圾回收以释放内存。

  • 自动触发:当根缓冲区中的对象数量达到设定的阈值时,PHP 会自动触发垃圾回收过程。这是 PHP 内存管理的一部分,旨在确保不会因循环引用导致内存泄漏。

PHP 的垃圾回收主要依赖于 引用计数标记-清除算法。当变量的引用计数降为零时,内存会立即释放。为了处理循环引用问题,PHP 会在特定条件下(例如达到根缓冲区的阈值或手动触发)执行标记-清除算法进行垃圾回收。通过这些机制,PHP 能够有效地管理内存,防止内存泄漏。

对象放入根缓冲区的时机

在 PHP 的垃圾回收(GC)机制中,将对象放入根缓冲区的时机和过程如下:

  1. 对象创建时

    当 PHP 创建新的对象时,它们可能会被立即放入根缓冲区。这通常发生在以下情况下:

    • 全局变量:全局变量是程序的根对象,因此它们在程序启动时会被放入根缓冲区。

    • 静态变量:静态变量在函数或方法中使用,也会被放入根缓冲区。

    • 注册对象:某些扩展或用户代码可能会注册对象,使其成为根对象。

  2. 对象的引用被增加

    当对象的引用计数增加时(例如,将对象赋值给新的变量),该对象可能会被添加到根缓冲区,以防止它在垃圾回收期间被错误地释放。尤其是在对象被用作全局变量或静态变量时,这种情况非常常见。

  3. 标记阶段

    在垃圾回收的标记阶段,根缓冲区中的对象被标记为根对象。根缓冲区提供了垃圾回收系统可以使用的稳定的起点,这些对象作为垃圾回收的根节点开始被扫描。以下是一些具体情况:

    • 根对象标记:所有根对象(如全局变量、静态变量等)在标记阶段会被扫描,并从根缓冲区开始标记。

    • 栈对象:函数调用的局部变量对象也可能被标记为根对象。

  4. 对象从根缓冲区移除

    当对象被从根缓冲区中移除时,通常发生在以下情况:

    • 对象生命周期结束:例如,全局变量或静态变量的作用域结束,或者它们被显式销毁时。

    • 垃圾回收:在垃圾回收的过程中,如果对象被标记为不再需要,它们可能会被从根缓冲区移除。

  5. 处理根缓冲区

    根缓冲区的处理过程主要包括:

    • 标记根对象:gc_mark_roots 函数会遍历根缓冲区中的对象,并将它们标记为活动对象。

    • 扫描根对象:gc_scan_roots 函数扫描根缓冲区,找出所有引用的对象。

    • 收集根对象:gc_collect_roots 函数处理根对象,并进行必要的清理。

  6. 额外的情况

    某些扩展或底层操作可能会手动将对象放入根缓冲区。例如,在扩展中直接操作对象生命周期或将对象添加到特定的根缓冲区。

对象放入根缓冲区的时机包括对象创建、引用计数增加、标记阶段等。这些操作确保对象在垃圾回收过程中不会被错误地回收,并帮助垃圾回收系统正确地管理对象生命周期。