php底层原理之垃圾回收机制

PHP 是一门托管型语言,在 PHP 编程中,程序员不需要手工处理内存资源的分配与释放(使用 C 编写 PHP 或 Zend 扩展除外),这就意味着 PHP 本身实现了垃圾回收机制(Garbage Collection)。在 PHP 官方网站可以看到对垃圾回收机制的介绍。

变量的内部实现

变量是一个语言实现的基础,变量有两个组成部分:变量名、变量值,PHP中可以将其对应为:zval、zend_value,这两个概念一定要区分开,PHP中变量的内存是通过引用计数进行管理的,而且PHP7中引用计数是在zend_value而不是zval上,变量之间的传递、赋值通常也是针对zend_value。

PHP中可以通过关键词定义一个变量:a;,在定义的同时可以进行初始化:$a = "hi~";,注意这实际是两步:定义、初始化,只定义一个变量也是可以的,可以不给它赋值,比如:

$a;
$b = 1;

这段代码在执行时会分配两个zval。
接下来我们具体看下变量的结构以及不同类型的实现。

//zend_types.h
typedef struct _zval_struct     zval;

typedef union _zend_value {
    zend_long         lval;    //int整形
    double            dval;    //浮点型
    zend_refcounted  *counted;
    zend_string      *str;     //string字符串
    zend_array       *arr;     //array数组
    zend_object      *obj;     //object对象
    zend_resource    *res;     //resource资源类型
    zend_reference   *ref;     //引用类型,通过&$var_name定义的
    zend_ast_ref     *ast;     //下面几个都是内核使用的value
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

struct _zval_struct {
    zend_value        value; //变量实际的value
    union {
        struct {
            ZEND_ENDIAN_LOHI_4( //这个是为了兼容大小字节序,小字节序就是下面的顺序,大字节序则下面4个顺序翻转
                zend_uchar    type,         //变量类型
                zend_uchar    type_flags,  //类型掩码,不同的类型会有不同的几种属性,内存管理会用到
                zend_uchar    const_flags,
                zend_uchar    reserved)     //call info,zend执行流程会用到
        } v;
        uint32_t type_info; //上面4个值的组合值,可以直接根据type_info取到4个对应位置的值
    } u1;
    union {
        uint32_t     var_flags;
        uint32_t     next;                 //哈希表中解决哈希冲突时用到
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
    } u2; //一些辅助值
};

zval结构比较简单,内嵌一个union类型的zend_value保存具体变量类型的值或指针,zval中还有两个union:u1、u2:

  • u1: 它的意义比较直观,变量的类型就通过u1.v.type区分,另外一个值type_flags为类型掩码,在变量的内存管理、gc机制中会用到,第三部分会详细分析,至于后面两个const_flags、reserved暂且不管
  • u2: 这个值纯粹是个辅助值,假如zval只有:value、u1两个值,整个zval的大小也会对齐到16byte,既然不管有没有u2大小都是16byte,把多余的4byte拿出来用于一些特殊用途还是很划算的,比如next在哈希表解决哈希冲突时会用到,还有fe_pos在foreach会用到......
    从zend_value可以看出,除long、double类型直接存储值外,其它类型都为指针,指向各自的结构。

变量的存储结构

  • 标量类型
    最简单的类型是true、false、long、double、null,其中true、false、null没有value,直接根据type区分,而long、double的值则直接存在value中:zend_long、double,也就是标量类型不需要额外的value指针。
  • 字符串
    PHP中字符串通过zend_string表示:
struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong        h;                /* hash value */
    size_t            len;
    char              val[1];
};

gc: 变量引用信息,比如当前value的引用数,所有用到引用计数的变量类型都会有这个结构,3.1节会详细分析
h: 哈希值,数组中计算索引时会用到
len: 字符串长度,通过这个值保证二进制安全
val: 字符串内容,变长struct,分配时按len长度申请内存
事实上字符串又可具体分为几类:IS_STR_PERSISTENT(通过malloc分配的)、IS_STR_INTERNED(php代码里写的一些字面量,比如函数名、变量值)、IS_STR_PERMANENT(永久值,生命周期大于request)、IS_STR_CONSTANT(常量)、IS_STR_CONSTANT_UNQUALIFIED,这个信息通过flag保存:zval.value->gc.u.flags,后面用到的时候再具体分析。

  • 数组
    array是PHP中非常强大的一个数据结构,它的底层实现就是普通的有序HashTable,这里简单看下它的结构
typedef struct _zend_array HashTable;

struct _zend_array {
    zend_refcounted_h gc; //引用计数信息,与字符串相同
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    reserve)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask; //计算bucket索引时的掩码
    Bucket           *arData; //bucket数组
    uint32_t          nNumUsed; //已用bucket数
    uint32_t          nNumOfElements; //已有元素数,nNumOfElements <= nNumUsed,因为删除的并不是直接从arData中移除
    uint32_t          nTableSize; //数组的大小,为2^n
    uint32_t          nInternalPointer; //数值索引
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};
  • 对象/资源
struct _zend_object {
    zend_refcounted_h gc;
    uint32_t          handle;
    zend_class_entry *ce; //对象对应的class类
    const zend_object_handlers *handlers;
    HashTable        *properties; //对象属性哈希表
    zval              properties_table[1];
};

struct _zend_resource {
    zend_refcounted_h gc;
    int               handle;
    int               type;
    void             *ptr;
};

对象比较常见,资源指的是tcp连接、文件句柄等等类型;

  • 引用
    引用是PHP中比较特殊的一种类型,它实际是指向另外一个PHP变量,对它的修改会直接改动实际指向的zval,可以简单的理解为C中的指针,在PHP中通过&操作符产生一个引用变量,也就是说不管以前的类型是什么,&首先会创建一个zend_reference结构,其内嵌了一个zval,这个zval的value指向原来zval的value(如果是布尔、整形、浮点则直接复制原来的值),然后将原zval的类型修改为IS_REFERENCE,原zval的value指向新创建的zend_reference结构。
struct _zend_reference {
    zend_refcounted_h gc;
    zval              val;
};

结构非常简单,除了公共部分zend_refcounted_h外只有一个val,举个示例看下具体的结构关系:

$a = "time:" . time();      //$a    -> zend_string_1(refcount=1)
xdebug_debug_zval('a');
$b = &$a;                   //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
xdebug_debug_zval('a');

b = &a这时候a、b的类型是引用,但是c =b并不会直接将b赋值给c,而是把b实际指向的zval赋值给c,如果想要$c也是一个引用则需要这么操作:

$a = "time:" . time();      //$a       -> zend_string_1(refcount=1)
$b = &$a;                   //$a,$b    -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
$c = &$b;/*或$c = &$a*/     //$a,$b,$c -> zend_reference_1(refcount=3) -> zend_string_1(refcount=1) 

这个也表示PHP中的 引用只可能有一层 ,不会出现一个引用指向另外一个引用的情况 ,也就是没有C语言中指针的指针的概念。

内存管理

接下来分析下变量的分配、销毁。

在分析变量内存管理之前我们先自己想一下可能的实现方案,最简单的处理方式:定义变量时alloc一个zval及对应的value结构(ref/arr/str/res...),赋值、函数传参时硬拷贝一个副本,这样各变量最终的值完全都是独立的,不会出现多个变量同时共用一个value的情况,在执行完以后直接将各变量及value结构free掉。

这种方式是可行的,而且内存管理也很简单,但是,硬拷贝带来的一个问题是效率低,比如我们定义了一个变量然后赋值给另外一个变量,可能后面都只是只读操作,假如硬拷贝的话就会有多余的一份数据,这个问题的解决方案是: 引用计数+写时复制 。PHP变量的管理正是基于这两点实现的。

  • 引用计数
    引用计数是指在value中增加一个字段refcount记录指向当前value的数量,变量复制、函数传参时并不直接硬拷贝一份value数据,而是将refcount++,变量销毁时将refcount--,等到refcount减为0时表示已经没有变量引用这个value,将它销毁即可。
$a = "time:" . time();   //$a       ->  zend_string_1(refcount=1)
$b = $a;                 //$a,$b    ->  zend_string_1(refcount=2)
$c = $b;                 //$a,$b,$c ->  zend_string_1(refcount=3)
unset($b);               //$b = IS_UNDEF  $a,$c ->  zend_string_1(refcount=2)

引用计数的信息位于给具体value结构的gc中:

typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

从上面的zend_value结构可以看出并不是所有的数据类型都会用到引用计数,long、double直接都是硬拷贝,只有value是指针的那几种类型才可能会用到引用计数。

下面再看一个例子:

$a = "hi~";
$b = $a;

猜测一下变量a/b的引用情况。
这个不跟上面的例子一样吗?字符串"hi~"有a/b两个引用,所以zend_string1(refcount=2)。但是这是错的,gdb调试发现上面例子zend_string的引用计数为0。这是为什么呢?

a: (refcount=0, is_ref=0)='hi~'

事实上并不是所有的PHP变量都会用到引用计数,标量:true/false/double/long/null是硬拷贝自然不需要这种机制,但是除了这几个还有两个特殊的类型也不会用到:interned string(内部字符串,就是上面提到的字符串flag:IS_STR_INTERNED)、immutable array,它们的type是IS_STRING、IS_ARRAY,与普通string、array类型相同,那怎么区分一个value是否支持引用计数呢?还记得zval.u1中那个类型掩码type_flag吗?正是通过这个字段标识的,这个字段除了标识value是否支持引用计数外还有其它几个标识位,按位分割,注意:type_flag与zval.value->gc.u.flag不是一个值。
支持引用计数的value类型其zval.u1.type_flag 包含 (注意是&,不是等于)IS_TYPE_REFCOUNTED:

|     type       | refcounted |
+----------------+------------+
|simple types    |            |
|string          |      Y     |
|interned string |            |
|array           |      Y     |
|immutable array |            |
|object          |      Y     |
|resource        |      Y     |
|reference       |      Y     |

simple types很显然用不到,不再解释,string、array、object、resource、reference有引用计数机制也很容易理解,下面具体解释下另外两个特殊的类型:

interned string: 内部字符串,这是种什么类型?我们在PHP中写的所有字符都可以认为是这种类型,比如function name、class name、variable name、静态字符串等等,我们这样定义:$a = "hi~";后面的字符串内容是唯一不变的,这些字符串等同于C语言中定义在静态变量区的字符串:char *a = "hi~";,这些字符串的生命周期为request期间,request完成后会统一销毁释放,自然也就无需在运行期间通过引用计数管理内存。

immutable array:不可变数组。

  • 但是当使用 & 操作符进行引用拷贝时,情况就不一样了:
    1.PHP 为 & 操作符操作的变量申请一个 zend_reference结构
    2.将 zend_reference.value 指向原来的 zval_struct.value
    3.zval_struct.value的数据类型会被修改为 zend_refrence
    4.将 zval_struct.value指向刚刚申请并初始化后的 zend_reference
    5.为新变量申请 zval_struct结构,将他的 value 指向刚刚创建的 zend_reference
    此时: var_int_2 都拥有一个 zval_struct结构体,并且他们的 zval_struct.value都指向了同一个 zend_reference结构,所以该结构的引用计数器的值为 2。

  • 为什么初始数组的引用计数器的值为 2?
    这牵扯到 PHP7 中的另一个概念,叫做 immutable array (不可变数组)。 关于 immutable array 的详细介绍我放到下篇文章中讲,这里我们只需要知道,这样定义的数组,叫做 不可变数组 。不可变数组和我们上面讲到的 内部字符串 一样,都是 不使用引用计数 的,但是不同点是,内部字符串的计数值恒为 0,而不可变数组会使用一个 伪计数值 2。

  • 写时复制

上一小节介绍了引用计数,多个变量可能指向同一个value,然后通过refcount统计引用数,这时候如果其中一个变量试图更改value的内容则会重新拷贝一份value修改,同时断开旧的指向,写时复制的机制在计算机系统中有非常广的应用,它只有在必要的时候(写)才会发生硬拷贝,可以很好的提高效率,下面从示例看下:

$a = array(1,2);
$b = &$a;
$c = $a;

//发生分离
$b[] = 3;
image.png

不是所有类型都可以copy的,比如对象、资源,事实上只有string、array两种支持,与引用计数相同,也是通过zval.u1.type_flag标识value是否可复制的:

#define IS_TYPE_COPYABLE         (1<<4)
|     type       |  copyable  |
+----------------+------------+
|simple types    |            |
|string          |      Y     |
|interned string |            |
|array           |      Y     |
|immutable array |            |
|object          |            |
|resource        |            |
|reference       |            |

  • 变量回收
    PHP变量的回收主要有两种:主动销毁、自动销毁。主动销毁指的就是 unset ,而自动销毁就是PHP的自动管理机制,在return时减掉局部变量的refcount,即使没有显式的return,PHP也会自动给加上这个操作,另外一个就是写时复制时会断开原来value的指向,这时候也会检查断开后旧value的refcount。
  • 垃圾回收
    PHP变量的回收是根据refcount实现的,当unset、return时会将变量的引用计数减掉,如果refcount减到0则直接释放value,这是变量的简单gc过程,但是实际过程中出现gc无法回收导致内存泄漏的bug,先看下一个例子:
$a = [1];
$a[] = &$a;

unset($a);

image.png

可以看到,unset(a,所以refcount > 0,无法通过简单的gc机制回收,这种变量就是垃圾,垃圾回收器要处理的就是这种情况,目前垃圾只会出现在array、object两种类型中,所以只会针对这两种情况作特殊处理:当销毁一个变量时,如果发现减掉refcount后仍然大于0,且类型是IS_ARRAY、IS_OBJECT则将此value放入gc可能垃圾双向链表中,等这个链表达到一定数量后启动检查程序将所有变量检查一遍,如果确定是垃圾则销毁释放。
标识变量是否需要回收也是通过u1.type_flag区分的:

#define IS_TYPE_COLLECTABLE
|     type       | collectable |
+----------------+-------------+
|simple types    |             |
|string          |             |
|interned string |             |
|array           |      Y      |
|immutable array |             |
|object          |      Y      |
|resource        |             |
|reference       |             |

小结:
如果一个变量value的refcount减少到0, 那么此value可以被释放掉,不属于垃圾
如果一个变量value的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾;
变量是否加入垃圾检查buffer并不是根据zval的类型判断的,而是与前面介绍的是否用到引用计数一样通过zval.u1.type_flag记录的,只有包含IS_TYPE_COLLECTABLE的变量才会被GC收集。

目前垃圾只会出现在array、object两种类型中,数组的情况上面已经介绍了,object的情况则是成员属性引用对象本身导致的,其它类型不会出现这种变量中的成员引用变量自身的情况,所以垃圾回收只会处理这两种类型的变量。

  • 回收过程
    如果当变量的refcount减少后大于0,PHP并不会立即进行对这个变量进行垃圾鉴定,而是放入一个缓冲buffer中,等这个buffer满了以后(10000个值)再统一进行处理,加入buffer的是变量zend_value的zend_refcounted_h:
typedef struct _zend_refcounted_h {
    uint32_t         refcount; //记录zend_value的引用数
    union {
        struct {
            zend_uchar    type,  //zend_value的类型,与zval.u1.type一致
            zend_uchar    flags, 
            uint16_t      gc_info //GC信息,垃圾回收的过程会用到
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

一个变量只能加入一次buffer,为了防止重复加入,变量加入后会把zend_refcounted_h.gc_info置为GC_PURPLE,即标为紫色,下次refcount减少时如果发现已经加入过了则不再重复插入。垃圾缓存区是一个双向链表,等到缓存区满了以后则启动垃圾检查过程:遍历缓存区,再对当前变量的所有成员进行遍历,然后把成员的refcount减1(如果成员还包含子成员则也进行递归遍历,其实就是深度优先的遍历),最后再检查当前变量的引用,如果减为了0则为垃圾。这个算法的原理很简单,垃圾是由于成员引用自身导致的,那么就对所有的成员减一遍引用,结果如果发现变量本身refcount变为了0则就表明其引用全部来自自身成员。具体的过程如下:

(1) 从buffer链表的roots开始遍历,把当前value标为灰色(zend_refcounted_h.gc_info置为GC_GREY),然后对当前value的成员进行深度优先遍历,把成员value的refcount减1,并且也标为灰色;

(2) 重复遍历buffer链表,检查当前value引用是否为0,为0则表示确实是垃圾,把它标为白色(GC_WHITE),如果不为0则排除了引用全部来自自身成员的可能,表示还有外部的引用,并不是垃圾,这时候因为步骤(1)对成员进行了refcount减1操作,需要再还原回去,对所有成员进行深度遍历,把成员refcount加1,同时标为黑色;

(3) 再次遍历buffer链表,将非GC_WHITE的节点从roots链表中删除,最终roots链表中全部为真正的垃圾,最后将这些垃圾清除。

  • 垃圾收集的内部实现
    接下来我们简单看下垃圾回收的内部实现,垃圾收集器的全局数据结构:
typedef struct _zend_gc_globals {
    zend_bool         gc_enabled; //是否启用gc
    zend_bool         gc_active;  //是否在垃圾检查过程中
    zend_bool         gc_full;    //缓存区是否已满

    gc_root_buffer   *buf;   //启动时分配的用于保存可能垃圾的缓存区
    gc_root_buffer    roots; //指向buf中最新加入的一个可能垃圾
    gc_root_buffer   *unused;//指向buf中没有使用的buffer
    gc_root_buffer   *first_unused; //指向buf中第一个没有使用的buffer
    gc_root_buffer   *last_unused; //指向buf尾部

    gc_root_buffer    to_free;  //待释放的垃圾
    gc_root_buffer   *next_to_free;

    uint32_t gc_runs;   //统计gc运行次数
    uint32_t collected; //统计已回收的垃圾数
} zend_gc_globals;

typedef struct _gc_root_buffer {
    zend_refcounted          *ref; //每个zend_value的gc信息
    struct _gc_root_buffer   *next;
    struct _gc_root_buffer   *prev;
    uint32_t                 refcount;
} gc_root_buffer;

zend_gc_globals是垃圾回收过程中主要用到的一个结构,用来保存垃圾回收器的所有信息,比如垃圾缓存区;gc_root_buffer用来保存每个可能是垃圾的变量,它实际就是整个垃圾收集buffer链表的元素,当GC收集一个变量时会创建一个gc_root_buffer,插入链表。
zend_gc_globals这个结构中有几个关键成员:

(1)buf: 前面已经说过,当refcount减少后如果大于0那么就会将这个变量的value加入GC的垃圾缓存区,buf就是这个缓存区,它实际是一块连续的内存,在GC初始化时一次性分配了10001个gc_root_buffer,插入变量时直接从buf中取出可用节点;
(2)roots: 垃圾缓存链表的头部,启动GC检查的过程就是从roots开始遍历的;
(3)first_unused: 指向buf中第一个可用的节点,初始化时这个值为1而不是0,因为第一个gc_root_buffer保留没有使用,有元素插入roots时如果first_unused还没有到达buf的尾部则返回first_unused给最新的元素,然后first_unused++,直到last_unused,比如现在已经加入了2个可能的垃圾变量,则对应的结构:


image.png

(4)last_unused: 与first_unused类似,指向buf末尾
(5)unused: GC收集变量时会依次从buf中获取可用的gc_root_buffer,这种情况直接取first_unused即可,但是有些变量加入垃圾缓存区之后其refcount又减为0了,这种情况就需要从roots中删掉,因为它不可能是垃圾,这样就导致roots链表并不是像buf分配的那样是连续的,中间会出现一些开始加入后面又删除的节点,这些节点就通过unused串成一个单链表,unused指向链表尾部,下次有新的变量插入roots时优先使用unused的这些节点,其次才是first_unused的
此GC机制可以通过php.ini中zend.enable_gc设置是否开启,如果开启则在php.ini解析后调用gc_init()进行GC初始化:

ZEND_API void gc_init(void)
{
    if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
        //分配buf缓存区内存,大小为GC_ROOT_BUFFER_MAX_ENTRIES(10001),其中第1个保留不被使用
        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_G的初始化,其中:GC_G(first_unused) = GC_G(buf) + 1;从第2个开始的,第1个保留
        gc_reset();
    }
}

在PHP的执行过程中,如果发现array、object减掉refcount后大于0则会调用gc_possible_root()将zend_value的gc头部加入GC垃圾缓存区:

ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
{
    gc_root_buffer *newRoot;

    //插入的节点必须是GC_BLACK,防止重复插入
    ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK));

    newRoot = GC_G(unused); //先看下unused中有没有可用的
    if (newRoot) {
        //有的话先用unused的,然后将GC_G(unused)指向单链表的下一个
        GC_G(unused) = newRoot->prev;
    } else if (GC_G(first_unused) != GC_G(last_unused)) {
        //unused没有可用的,且buf中还有可用的
        newRoot = GC_G(first_unused);
        GC_G(first_unused)++;
    } else {
        //buf缓存区已满,这时需要启动垃圾检查程序了,遍历roots,将真正的垃圾释放
        //垃圾回收的动作就是在这触发的
        if (!GC_G(gc_enabled)) {
            return;
        }
        ...

        //启动垃圾回收过程
        gc_collect_cycles(); //即:zend_gc_collect_cycles()
        ...
    } 

    //将插入的ref标为紫色,防止重复插入
    GC_TRACE_SET_COLOR(ref, GC_PURPLE);
    //注意:gc_info不仅仅只有颜色的信息,还会记录当前gc_root_buffer在整个buf中的位置
    //这样做的目的是可以直接根据zend_value的gc信息取到它的gc_root_buffer,便于进行删除操作
    GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
    newRoot->ref = ref;

    //GC_G(roots).next指向新插入的元素
    newRoot->next = GC_G(roots).next;
    newRoot->prev = &GC_G(roots);
    GC_G(roots).next->prev = newRoot;
    GC_G(roots).next = newRoot;
}

同一个zend_value只会插入一次,再次插入时如果发现其gc_info不是GC_BLACK则直接跳过。另外像上面示例1的情况,插入后如果后面发现其refcount减为0了则表明它可以直接被回收掉,这时需要把这个节点从roots链表中删除,删除的操作通过GC_REMOVE_FROM_BUFFER()宏操作:

#define GC_REMOVE_FROM_BUFFER(p) do { \
    zend_refcounted *_p = (zend_refcounted*)(p); \
        if (GC_ADDRESS(GC_INFO(_p))) { \
            gc_remove_from_buffer(_p); \
        } \
} while (0)

ZEND_API void ZEND_FASTCALL gc_remove_from_buffer(zend_refcounted *ref)
{
    gc_root_buffer *root;

    //GC_ADDRESS就是获取节点在缓存区中的位置,因为删除时输入是zend_refcounted
    //而缓存链表的节点类型是gc_root_buffer
    root = GC_G(buf) + GC_ADDRESS(GC_INFO(ref));
    if (GC_REF_GET_COLOR(ref) != GC_BLACK) {
        GC_TRACE_SET_COLOR(ref, GC_PURPLE);
    }
    GC_INFO(ref) = 0;
    GC_REMOVE_FROM_ROOTS(root); //双向链表的删除操作
    ...
}

插入时如果发现垃圾缓存链表已经满了,则会启动垃圾回收过程:zend_gc_collect_cycles(),这个过程会对之前插入缓存区的变量进行判断是否是循环引用导致的真正的垃圾,如果是垃圾则会进行回收,回收的过程前面已经介绍过:

ZEND_API int zend_gc_collect_cycles(void)
{
    ...
    //(1)遍历roots链表,对当前节点value的所有成员(如数组元素、成员属性)进行深度优先遍历把成员refcount减1
    gc_mark_roots();

    //(2)再次遍历roots链表,检查各节点当前refcount是否为0,是的话标为白色,表示是垃圾,不是的话需要对还原(1),把refcount再加回去
    gc_scan_roots();

    //(3)将roots链表中的非白色节点删除,之后roots链表中全部是真正的垃圾,将垃圾链表转到to_free等待释放
    count = gc_collect_roots(&gc_flags, &additional_buffer);
    ...
    
    //(4)释放垃圾
    current = to_free.next;
    while (current != &to_free) {
        p = current->ref;
        GC_G(next_to_free) = current->next;
        if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_OBJECT) {
            //调用free_obj释放对象
            obj->handlers->free_obj(obj);
            ...
        } else if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_ARRAY) {
            //释放数组
            zend_array *arr = (zend_array*)p;

            GC_TYPE(arr) = IS_NULL;
            zend_hash_destroy(arr);
        }
        current = GC_G(next_to_free);
    }
    ...
}

各步骤具体的操作不再详细展开,这里单独说明下value成员的遍历,array比较好理解,所有成员都在arData数组中,直接遍历arData即可,如果各元素仍是array、object或者引用则一直递归进行深度优先遍历;object的成员指的成员属性(不包括静态属性、常量,它们属于类而不属于对象),前面介绍对象的实现时曾说过,成员属性除了明确的在类中定义的那些外还可以动态创建,动态属性保存于zend_obejct->properties哈希表中,普通属性保存于zend_object.properties_table数组中,这样以来object的成员就分散在两个位置,那么遍历时是分别遍历吗?答案是否定的。

实际前面已经简单提过,在创建动态属性时会把全部普通属性也加到zend_obejct->properties哈希表中,指向原zend_object.properties_table中的属性,这样一来GC遍历object的成员时就可以像array那样遍历zend_obejct->properties即可,GC获取object成员的操作由get_gc(即:zend_std_get_gc())完成:

ZEND_API HashTable *zend_std_get_gc(zval *object, zval **table, int *n)
{   
    if (Z_OBJ_HANDLER_P(object, get_properties) != zend_std_get_properties) {
        *table = NULL;
        *n = 0;                             
        return Z_OBJ_HANDLER_P(object, get_properties)(object);
    } else {
        zend_object *zobj = Z_OBJ_P(object);

        if (zobj->properties) {             
            //有动态属性
            *table = NULL;
            *n = 0;
            return zobj->properties;
        } else {
            //没有定义过动态属性,返回数组
            *table = zobj->properties_table;
            *n = zobj->ce->default_properties_count;
            return NULL;
        }
    }
}

///////////////////////////////////////////////////////////////////////////////////////
我们定义一个PHP变量如下:

 $var = "laruence";
 $var_dup = $var;
 unset($var);

第一行代码创建了一个字符串变量,申请了一个大小为9字节的内存,保存了字符串”laruence”和一个NULL(\0)的结尾。
第二行定义了一个新的字符串变量,并将变量var的值”复制”给这个新的变量(此时并没有开辟空间)。

$a = range(0,1000);
$b = $a;//在 PHP 中,我们使用『写时复制(copy-on-write)』模型。
$a=$a = range(0,2000);
var_dump(memory_get_usage());//返回分配给 PHP 的内存量


$a = 123;
xdebug_debug_zval('a'); // a:(refcount=0, is_ref=0)int 123

$b = $a;
xdebug_debug_zval('a'); // a:(refcount=0, is_ref=0)int 123

$c = &$a;
xdebug_debug_zval('a'); // a:(refcount=2, is_ref=1)int 123
//refcount__gc   表示引用计数 
//is_ref__gc 表示是否为引用     
//数组  array和object变量
会产生元素个数+1的变量容器
$a = range(0,3);
xdebug_debug_zval('a');

$b = $a;
xdebug_debug_zval('a');

$c = &$a;
xdebug_debug_zval('a');

//对象
class Person
{
    public $name = "zhangsan";
}

$p1 = new Person;
xdebug_debug_zval('p1');

$p2 = $p1;
xdebug_debug_zval('p1');

$p2->name = "zhouliuwan";
xdebug_debug_zval('p1'); 


第三行unset了变量var

说说 PHP 的 die 和 exit

exit 和 die 有区别吗?
翻看php的源码,在 zend_language_scanner.l 中,很容易就能发现这关键字是同一个 token:

/home/guofu/php-7.2.21/sapi/cli/php_cli.c
/home/guofu/php-7.2.21/Zend  在该目录下
1125 <ST_IN_SCRIPTING>"exit" {
1126         RETURN_TOKEN(T_EXIT);
1127 }
1128 
1129 <ST_IN_SCRIPTING>"die" {
1130         RETURN_TOKEN(T_EXIT);
1131 }

用exit输出int类型会发生什么?

exit(1);
exit('1');
exit(256);
exit(257);

//a. 第一种情况,执行完成
function test() 
{ 
 echo '这个是中止方法test的输出'; 
} 
 
register_shutdown_function('test'); 
 
echo 'before' . PHP_EOL;

//b. 第二种情况,exit/die导致的中止
function test() 
{ 
 echo '这个是中止方法test的输出'; 
} 
 
register_shutdown_function('test'); 
 
echo 'before' . PHP_EOL; 
exit(); 
echo 'after' . PHP_EOL; 
//c. 第三种情况,发送致命错误中止
function test() 
{ 
 echo '这个是中止方法test的输出'; 
} 
 
register_shutdown_function('test'); 
 
echo 'before' . PHP_EOL; 
 
// 这里会发生致命错误 
$a = new a(); 
 
echo 'after' . PHP_EOL;

参考文章

说说 PHP 的 die 和 exit

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容