php垃圾回收之引用计数

一、引用计数基本知识

    每个php变量存在一个叫“zval”的变量容器中,一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。第一个是“is_ref",是个bool值,用来标识这个变量是否属于引用集合。通过这个字节,php引擎才能把普通变量和引用变量区分开来,由于php允许用户通过使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制来优化内存使用。第二个额外字节是”refcount“,用以表示指向这个zval变量容器的变量个数。所有的符号存在一个符号表中,其中每个符号都有作用域(脚本中的函数或方法也都有作用域)。

   ① 当一个变量被赋常量值时,就会变成一个zval变量容器,如下:

$a = "new string";
xdebug_debug_zval('a');
//输出结果如下:
a: (refcount=1, is_ref=0)='new string'

    在上例中,新的变量a,是当前作用域中生成的。并且生成了类型为String和值为new string的变量容器。在额外的两个字节信息中,”is_ref"被默认设置为FALSE,因为没有任何自定义的引用生成。“refcount"被设定为1,因为这里只有一个变量使用这个变量容器。注意到当”refcount"的值是1时,“is_ref”的值总是false。

    ②当一个变量赋值给另一变量将增加引用次数(refcount),如下:

$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
//输出:
a: (refcount=2, is_ref=0)='new string'

    这时,引用次数是2,因为同一个变量容器被变量a和变量b关联。当没必要时,php不会去复制已生成的变量容器。变量容器在“refcount”变成0时就被销毁。

    ③当任何关联到某个变量容器的变量离开它的作用域(比如:函数执行结束),或者对变量调用了函数unset()时,“refcount”就会减1,如下:

$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );
//输出结果
a: (refcount=3, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'

    如果再执行unset($a),包含类型和值的这个变量容器就会从内存中删除。

二、复合类型

   ① 当考虑像array和Object这样的符合类型时,事情就复杂了。与标量类型的值不同,array和Object类型的变量把他们的成员或属性存在自己的符号表中。这意味着下面的例子将生成三个zval变量容器:

$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
//输出如下:
a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=1, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42
)

 结构图如下:


   这三个zval变量容器是:a,meaning和number。增加和减少"refcount"的规则和上面提到的一样。

   ②下面,再在数组中添加一个元素,并把它的值设置为数组中已存在元素的值:

$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'
)

    结构图如下:


    从以上的xdebug输出信息,我们看到原有的数组元素和新添加的数组元素关联到同一个“refcount”2的zval变量容器。尽管xdebug的输出显示两个值为"life“的zval变量容器,其实是同一个。函数xdebug_debug_zval()不显示这个信息,但是通过显示内存指针信息就可以看到。

    ③删除数组中的一个元素,就是类似于从作用域中删除一个变量。删除后,数组中的这个元素所在的容器的refcount值减少,同样当refcount为0时,这个变量容器就从内存中被删除,看下面的例子:

$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
//输出信息为
a: (refcount=1, is_ref=0)=array (
   'life' => (refcount=1, is_ref=0)='life'
)

   ④ 当我们添加一个数组本身作为这个数组的元素时,事情就变得有趣,看下面的例子:

$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)=...
)

    结构图如下:


     能看到数组变量a同时也是这个数组的第二个元素(1)指向的变量容器中refcount为2。上面的输出结构中的”..."说明发生了递归操作,显然在这种情况下意味着"..."指向原始数组。

     ⑤对一个变量调用unset,将删除这个符号,且它指向的变量容器中的引用次数也减1.所以,如果我们在执行完④的代码后,对变量$a调用unset,那么变量$a和数组元素”1“所指向的变量容器的引用次数减1,从2变成1,如下例子:

(refcount=1, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=1, is_ref=1)=...
)

 结构图如下:


三、清理变量容器的问题----内存泄露

     从(二⑤)中可以看出,尽管不再有某个作用域中的任何符号指向这个结构(就是变量容器),由于数组元素”1“仍然指向数组本身,所以这个容器不会被清楚。因为没有另外的符号指向它,用户没有办法清楚这个结构,结果就会导致内存泄露。

     庆幸的是,php将在请求结束时清楚这个数据结构,但是在php清楚之前,将耗费不少空间内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这种的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更可能出现这种情况,因为对象总是隐式的被引用。

    如果上面的情况仅仅发生一两次倒也没有什么,但是如果出现几千次或更多,这显然是个大问题。在长时间运行的脚本,例如请求基本上不会结束的守护进程或者单元测试中的大的套件中,就有可能消耗掉大量的内存。

猜你喜欢

转载自student-lp.iteye.com/blog/2096298
今日推荐