php垃圾回收机制


php变量存储在叫zval的容器中,下文用变量容器说明,这个容器包含了变量类型,变量值,是否是引用变量is_ref ,容器引用次数refcount 四个部分。

引用计数机制

标准变量

将一个常量赋值给一个变量时就会创建一个变量容器,如下

<?php
$a = 'a string';
?>

如果你安装了Xdebug扩展,可以使用xdebug_debug_zval()查看效果

<?php
$a = 'a string';
xdebug_debug_zval('a');
?>

结果为

a: (refcount=1, is_ref=0)='a string'

如果把变量$a赋值给另一个变量$b,不会新创建新变量容器,但是引用次数会加1,如下

<?php
$a = 'a string';
$b = $a;
xdebug_debug_zval('a');
?>

结果为

a: (refcount=2, is_ref=0)='a string'

当变量离开作用域(比如所在函数执行完),或者使用unset()方法时,对应的变量容器的refcount会减1,当refcount为0时,变量容器销毁,内存回收。

复合变量

<?php
$a = array('meaning' => 'life', 'number' => 42);
xdebug_debug_zval('a');
?>

上面的将产生3个变量容器,ameaningnumberrefcount的加减规则和标准变量一样。

<?php
$a = array('meaning' => 'life', 'number' => 42);
$a['life'] = $a['meaning'];
xdebug_debug_zval('a');
?>

结果

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=2, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42,
   'life' => (refcount=2, is_ref=0)='life'
)

上面的过程并不会创建新的变量容器,meaninglife指向同一个变量容器,所以refcount为2,看下面的图更容易理解。

循环引用问题

<?php
$a = array('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)=...
)

上面的过程会创建2个变量容器,其中a和数组a索引为1的变量指向同一个变量容器,所以refcount为2,如下图

如果对数组a执行unset操作,变量容器会如下图所示

由于变量容器的refcount仍为1,不能为清除,同时由于没有变量在指向这个变量容器,没有办法对refcount进行减1操作,这个变量容器会一直存在直到脚本结束,如果有大量这种变量容器存在,很可能会造成内存泄漏。为了解决这个问题,php5.3以后引入了新的垃圾回收机制,GC(Garbage Collector)。

GC机制

关于垃圾回收,主要有3条基本原则

  • refcount在增加,不是垃圾
  • refcount减小为0,变量容器将被清除
  • refcount减小但是大于0,变量容器仍存在,此时将使用GC机制
    GC机制的整个过程如下图

下面对上面的ABCD步骤进行逐一解释
A: 为了避免每当refcount减少的时候都调用GC机制,算法会把可能的变量容器(zval)都放入根缓冲区(root buffer),用紫色标记,这些变量容器是疑似垃圾。仅仅当缓冲区满的时候,才会对这些变量容器进行垃圾回收操作。
B: 对根缓冲区内所有变量容器的refcount进行减1操作,为了避免对同一变量容器重复操作,减1后标记为灰色。
C: 检查根缓冲区内所有变量容器,refcount为0的标记为白色(垃圾),refcount大于0的,将refcount进行加1操作(对B操作的还原),标记为黑色。
D: 释放标记为白色的变量容器。

整个过程可以概括为疑似垃圾放入根缓冲区,减操作,恢复操作,清除垃圾。
注意,当$a = null时,a对应的变量容器的refcount为0。

参考

垃圾回收机制
php底层原理之垃圾回收机制
PHP的GC垃圾收集机制