PHP内核源码阅读记---round 4 变量的自动GC(垃圾回收)机制

在C/C++语言中,想要在堆上分配变量,需要手动进行内存分配与释放,这往往是一件很繁琐的事情,现代高级语言普遍提供了变量自动GC的机制。PHP同样也实现了这个机制,PHP使用'$'符声明变量后,不需要手动销毁,内核会自动进行释放。

一、引用计数和写时复制

如何实现自动GC呢,简单的实现方式:在函数定义变量时分配一块内存,用于保存zval及对应的value结构,在函数返回时再将内存释放。如果在函数执行期间,该变量作为参数调用了其他函数或复制给了其他变量,则把变量复制一份,变量之间相互独立,不冲突。

这种方法是可行的,但是这种深拷贝的方法带来了几个问题:效率低、内存浪费严重。有时候我们对变量仅仅做读操作。所以,在PHP中为了解决这个问题采用了引用计数+写时复制

指在变量赋值、传递时不是进行直接复制,而是多个变量指向同一个value容器,同时使用引用计数记录该value有多少个变量在使用;而当某一个变量需要进行修改时,它们不能继续使用同一个value,所以复制一份value进行修改,这就是写时复制。

引用计数

在PHP7中,变量的引用计数保存在zend_value中,与旧版本不同(旧版本保存在zval中)。不同的数据类型结构中都有一个相同的成员;zend_refcounted_h gc;。它就是用来保存引用计数的。其结构为:

typedef struct _zend_refcounted_h {
    // 引用计数
    uint32_t refcount;
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                // 类型
                zend_uchar type,
                zend_uchar flags,
                // 垃圾回收时用到
                uint16_t gc_info
            )
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

举例:

$a = array();   // $a       ->  zend_array(refcount=1)
$b = $a;        // $a,$b    =>  zend_array(refcount=2)
$c = $b;        // $a,$b,$c =>  zend_array(refcount=3)
unset($b);      // $a,$c    =>  zend_array(refcount=2) $b = IS_UNDEF

注意:并不是所有的数据类型都会用到引用计数,没有具体value结构的类型不会用到,例如:整型、浮点型、布尔型、NULL,它们的值直接通过zval保存,因此不会共用value,而是深拷贝。除此之外,还有两种特殊情况不会用到引用计数,分别是内部字符串、不可变数组

内部字符串:在PHP中写的函数名、类名、变量名、静态字符串都是这种类型。
不可变数组:是opCache优化出的一种类型,不作说明。

例如:

// 这种情况refcount都为0
$a = 'hi';
$b = $a;

// 这种情况refcount为2
$a = 'hi' . time();
$b = $a;

因为前者为静态字符串,后者为动态

写时复制

变量使用了引用计数必然会出现其中一个变量修改value的情况,这个时候需要对value进行分离,发生修改的变量会复制一份数据出来进行修改,同时断开原来value的指向,指向新的value。如:

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

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

并不是所有类型的value都进行复制,对象、资源是无法复制的,所以对对象、资源进行修改时,将会反映到所有变量上。

回收时机

在zval断开value的指向时,若发现refcount=0则会直接释放value。(断开指向发生在修改变量与函数返回时)。

循环引用

这个机制依然存在问题:当发生循环引用时,变量无法回收,将导致内存得不到释放,造成内存泄露。循环应用是指变量内部的成员引用了变量自身,当外部所有引用都断开时,该变量的refcount依旧大于0,而实际上变量已经未被使用了。

解决方案:

若一个变量value的refcount减少到0后,直接释放,若减少之后大于0,value因为可能是一个垃圾而被垃圾回收器收集起来。
当垃圾收集器中的可能垃圾达到一定数量后,就会启动垃圾鉴定:对value的所有成员减一遍引用计数,若value本身的refcount变为0,则表明该value为垃圾,并进行回收。