PHP中的垃圾回收机制

引用计数

前面有几篇文章提到,PHP中的变量都储存在’zval’这样一个容器中。在’zval’结构体中,存在is_ref和refcount两个元素分别来标志这个变量是否为引用集合(reference set)以及被引用的次数。我们很容易想到,既然refcount表示被引用的次数,那么当refcount=0的时候就表示没有变量指向这个zval容器,那么我们就可以将这个zval容器回收了。这就是引用计数垃圾回收机制。
但是,这样可能会发生循环引用的问题,会造成内存泄露。

1
2
3
4
5
6
7
<?php
//step1
$a = array( 'one' );
$a[] =& $a;
//step2
unset($a);
?>

创建一个数组类型变量$a,给变量$a新增一个元素,该元素为数组变量$a的一个引用。此时变量$a的refcount为2。

然后将数组a销毁,此时变量$a的refcount为1。尽管没有变量指向这个zval容器,它的引用计数依然为1,无法被垃圾回收,这样就会造成内存泄露。

回收周期(Collecting Cycles)

自PHP5.3之后,使用引用计数系统中的同步周期回收(Concurrent Cycle Collection in Reference Counted Systems)这种方法来处理内存泄露问题。这个算法比较复杂,它的基本规则就是如果一个引用计数增加,它将继续被使用,当然就不再在垃圾中。如果引用计数减少到零,所在变量容器将被清除(free)。就是说,仅仅在引用计数减少到非零值时,才会产生垃圾周期(garbage cycle)。其次,在一个垃圾周期中,通过检查引用计数是否减1,并且检查哪些变量容器的引用次数是零,来发现哪部分是垃圾。

为避免不得不检查所有引用计数可能减少的垃圾周期,这个算法把所有可能根(possible roots 都是zval变量容器),放在根缓冲区(root buffer)中(用紫色来标记,称为疑似垃圾),这样可以同时确保每个可能的垃圾根(possible garbage root)在缓冲区中只出现一次。仅仅在根缓冲区满了时,才对缓冲区内部所有不同的变量容器执行垃圾回收操作。看上图的步骤 A。

在步骤 B 中,模拟删除每个紫色变量。模拟删除时可能将不是紫色的普通变量引用数减”1”,如果某个普通变量引用计数变成0了,就对这个普通变量再做一次模拟删除。每个变量只能被模拟删除一次,模拟删除后标记为灰(原文说确保不会对同一个变量容器减两次”1”,不对的吧)。

在步骤 C 中,模拟恢复每个紫色变量。恢复是有条件的,当变量的引用计数大于0时才对其做模拟恢复。同样每个变量只能恢复一次,恢复后标记为黑,基本就是步骤 B 的逆运算。这样剩下的一堆没能恢复的就是该删除的蓝色节点了,在步骤 D 中遍历出来真的删除掉。

算法中都是模拟删除、模拟恢复、真的删除,都使用简单的遍历即可(最典型的深搜遍历)。复杂度为执行模拟操作的节点数正相关,不只是紫色的那些疑似垃圾变量。

以上的配图和步骤摘自PHP官方手册,估计大多数人也没看懂。我总结一下就是:对zval中的每一个元素的refcount进行-1操作,然后将减为0的元素模拟删除,如果此时zval的refcount变成了0,就说明仅仅只有它内部的元素指向了它自己,也就是发生了循环引用,这时候就可以将这个zval大胆的删掉了。

最后总结来看,PHP的GC机制有以下三点特性:

  1. 并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收。
  2. 可以解决循环引用问题。
  3. 可以总将内存泄露保持在一个阈值以下。

参考资料

PHP手册:垃圾回收机制