PHP 垃圾回收机制

PHP 5.3 之前使用 Reference Counting(引用计数)

很好理解,在 PHP 中当一个变量被赋值时会生成一个 zval 变量容器。zval 容器其中有两个字段 refcountis_ref。当 refcount 为 0 时,生成的 zval 容器会被释放,完成了垃圾回收。

zval 是 PHP 存放变量的容器。more
refcount 是计数器,每当有变量引用同一个 zval 变量容器时计数器就会加 1,当取消一个引用时变量就会减 1,当计数器为 0 时,表示这块 zval 内存没有被使用,随即被销毁,完成一次垃圾回收。 more
is_ref 是一个布尔类型,用来标示变量是否被引用。

# refcount 计数器示例
$a = "new string";
xdebug_debug_zval('a'); // 结果: a: (refcount=1, is_ref=0)='new string'
$b = $a;
xdebug_debug_zval('a'); // 结果: a: (refcount=2, is_ref=0)='new string'
unset ($b);
xdebug_debug_zval('a'); // 结果: a: (refcount=1, is_ref=0)='new string'
# is_ref 引用示例
$a = "new string";
xdebug_debug_zval('a'); // 结果: a: (refcount=1, is_ref=0)='new string'
$b = & $a;
xdebug_debug_zval('a'); // 结果: a: (refcount=2, is_ref=1)='new string'

但是 Reference Counting 存在缺陷,在引用自身 (循环引用) 时会造成内存泄露:

$a = [ 'one'];
$a [] =& $a;
xdebug_debug_zval( 'a' );

结果:
a: (refcount=2, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=2, is_ref=1)=...
)

img1
此时,refcount 为 2,当对 $a 执行 `unset` 后,变量 $a 从符号表中消失,但是由于 refcount 为被减 1,并且符号表中找不到对这块内存的引用,故 zval 不会被释放,所以就造成了内存泄露。
img2

PHP 5.3 之后使用 Collecting Cycles 算法(回收周期)

这个算法还是以引用计数为基础, 但比较复杂,简单点说:
首先 PHP 会分配一个固定大小的 “根缓冲区”,这个缓冲区用于存放固定数量的 zval,这个数量默认是 10,000,如果需要修改则需要修改源代码 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES 然后重新编译。
img3
从上图来看:
A 步骤把所有可能发生内存泄露的 zval 放入根缓冲区,并确保每个只出现一次。
B 步骤为模拟删除,把所有缓冲区的根 zval 遍历,并将对应的 refcount 减 1,并确保只模拟删除一次。
C 步骤为模拟恢复,把所有缓冲区的根 zval 遍历,找到 refcount 大于 0 的,并将对应的 refcount 加 1。
D 步骤把所有剩余 recount 为 0 的 zval 删除。
至此完成了一次垃圾回收。

两种垃圾回收的区别

  • PHP 5.3 之后是当缓冲区满时才进行垃圾回收,5.3 之前是只要某个 zvalrefcount 减为 0 时就回收。
  • 由上可知,PHP 5.3 之后的回收算法占用内存,5.3 之前的耗时间。
  • 新的垃圾回收机制将内存泄露控制在一个范围 (取决于缓冲区的大小)

可以看下 PHP 官网对两种算法的性能比较:性能考虑

参考:
PHP 的垃圾回收机制
垃圾回收机制

发表评论