原文地址: http://derickrethans.nl/collecting-garbage-phps-take-on-variables.html
关于PHP垃圾回收机制(Garbage Collection . GC) ,原作者写了三篇文章。这是第一篇,主要讲解PHP如何处理变量。
第二篇和第三篇主要讲常用的GC方法,以及GC是如何实现的,以及一些其它的说明和评测。
PHP版本:5.3
进入正题:
PHP把变量保存在zval容器里面。容器,container,可以想像成一块存储区域,或者一个盒子。
如上图所示,zval容器保存了此变量的类型type,值value,还有其它二块东西。
其中一个叫做”is_ref”, 它是一个bool型的值,占用一个bit,表示该zval容器(也就是这个变量)是否被引用。
php就是使用这个bit来判断变量是个普通变量,或者是个引用(reference)。
说起来,PHP有二种引用:
其一是用户代码中的&
其二是PHP内部实现的引用计数机制(internal reference counting mechanism),用来优化内存使用的。
is_ref是指&
zval容器中的另外一部分,是”refcount”,引用计数。用来记录有多少个变量指向这个zval容器。
通常吧,这个时候我们只说symbol(符号),不说variables(变量),其实是同一个意思。
当refcount为1的时候,is_ref必须为0。
当refcount为0的时候,该容器会被删除掉,释放空间出来。
所有的symbol保存在一张symbol table里面(符号表)。
php维护了很多张这样的表,GLOBAL一张,每个函数一张。类的每个方法也有一张。
基本上是按照变量的作用域(scope)来建表的。
当我们用一个constant value(常量)来为变量赋值的时候,zval容器才会被创建出来。
举例:
$a = "this is"; |
如上图所示;在当前的作用域里面,符号a被创建出来,zval容器也被创建,
类型:string
值: this is
is_ref: 默认为0
refcount: 1 表示现在只有一个符号(a)指向这个容器。
如果你安装了Xdebug,可以使用如下的代码:
xdebug_debug_zval("a"); |
可以看到:
a: (refcount=1, is_ref=0)="this is" |
貌似看不到变量类型嘛。
再看下面的例子:
$a = "this is"; $b = $a; $c = $a; xdebug_debug_zval("a"); |
可以看到:
a: (refcount=3, is_ref=0)="this is" |
如上图所示, refcount 变成3了,表示有3个符号指向这个容器。
看来PHP还是蛮聪明的,它并没有给$b创建一个单独的容器出来,节省了内存。
只有到了必要的时候,才会创建新的容器。
如下:
$a = "this is"; $b = $a; $c = $a; $c = 42; |
如上图所示, 在$c = 42的时候,一个新的容器被创建出来,原有容器的refcount减一。
如果我们unset一个变量呢?
$a = "this is"; $c = $b = $a; xdebug_debug_zval("a"); $c = 42; unset( $b, $c ); xdebug_debug_zval("a"); |
会显示:
a: (refcount=3, is_ref=0)="this is" a: (refcount=1, is_ref=0)="this is" |
如果我们再调用 unset($a),那么这个zval容器也就一起消失了。
接下来讲引用赋值:
$a = "this is" $b = &$a; $c = &$b; |
如图:符号a b和c都指向同一个容器,而且这个容器的is_ref位为1(因为&出现了)。
表示这是一个引用。refcount为3。
接下来
$b = 42; |
接下来unset一个变量
unset($c); |
再unset($a)呢?
refcount继续减一,值为1。同时, is_ref变回0。引用消失。a符号消失。
您理解了吗?
引用赋值与普通赋值的区别就在于容器的is_ref位为1还是为0。
如果为1,改变任意一个变量的值,只是会更改容器的值。
如果为0,改变任意一个变量的值,都会创建一个新的容器出来。
如果把引用赋值与普通赋值混合起来会怎么样?
$a = "this is"; $b = $a; |
继续
$c = &$b; |
如图,一个新的容器被分配出来,b和c都指向它,而且is_ref=1。
没什么特殊的。想想也应该是这样。
总不可能abc都指向同一个容器吧。
这样呢?
$a = "this is"; $b = &$a; |
如图,二个符号指向同一容器,is_ref=1,因为发生了引用赋值。
继续
$c = $a; |
如图,一个新的容器被分配出来,c指向了它。 原容器没发生变化。
看到这里,是不是有点乱了。慢慢体会一下吧。
休息一下。
接下来讲PHP如何处理数组和对象。
以数组为例吧,对象也差不多。
每个数组都维护了一张自己的符号表,保存了自己的元素。
看代码:
$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的处理,与普通的变量(标量 scalar)是相同的。演示一下看:
$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" ) |
如图,life和meaning都指向同一个容器,容器的refcount为2。
和前面所讲的php处理标量的行为是完全一致的。
unset数组的一个元素,与unset一个普通变量的情况也是完全一致的,
删除这个符号,然后refcount减一,如果值为0了,就删除这个容器。不再举例。
最后来看一个奇怪的操作:
我们把数组自身作为它自己的元素,并且使用引用赋值。
好象说得不太明白,看代码:
$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)=... ) |
如果unset a变量,会怎么样?
按照以前的经验,应该是会删除a这个符号,然后refcount减一。如果值为0了,就清理掉容器。
不过从直觉上来讲,$a被unset,那么这个数组也应该消失才对吧?
unset($a); |
输出:
(refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)="one", 1 => (refcount=1, is_ref=1)=... ) |
虽然在当前的作用域里面,已经没有符号指向这个zval容器,但它并没有被释放出来,
只是因为array[1]还指向着它?
因为没有变量指向这个容器了,所以在php代码中,我们无法对其进行任何操作。
这就出现了内存泄漏(memory leak)。
不过,在脚本执行结束之后,php会把这块区域也释放出来的。
所以,使用引用的时候,一定要小心。
如果你想继续研究,php如何处理function参数,function局部变量,以及更多的引用赋值,请看这个PDF:
http://derickrethans.nl/files/phparch-php-variables-article.pdf
不过要注意,他讲的是php4.3。不知道在5.3的时候,会不会有什么变化。
我就不再翻译了。
原文地址: http://derickrethans.nl/collecting-garbage-cleaning-up.html
前面讲过,关于PHP垃圾回收机制,原作者写了三篇文章,
第一篇讲的是 php垃圾回收机制之变量的处理。
这是第二篇,主要讲PHP5.3如何进行垃圾回收。
在上一篇里面,我们遇到了 circular reference的问题,导致了内存泄露。
本文主要来解决这个问题。
来回忆一下吧:
$a = array("one"); $a[] = &$a; unset($a); |
一直以来,reference counting memory mechanisms 引用计数机制都存在这个问题,
直到2007年,David F. Bacon和V.T. Rajan 写了一篇论文”Concurrent Cycle Collection in Reference Counted Systems“,解决了这个问题。
于是Derick Rethans以及某些人一起,按照这篇论文,在PHP 5.3版本源代码里面解决了circular reference导致内存泄露的问题。
下面简单讲解一下这套算法:
首先,介绍一下背景知识:
如果zval容器的refcount增加了,说明有变量(符号)正在使用它,所以它肯定不是垃圾。
如果zval容器的refcount减少到0了,这个容器会被删除掉,空间被释放出来。
综上所述,只有在refcount减少到一个非零值的时候,才有可能存在垃圾回收的问题。
如果上面讲的知识您不太明白,那么请去复习一下关于垃圾回收的第一篇文章:php垃圾回收机制之变量的处理。
其次,在垃圾回收过程中,怎样来分辨哪些zval容器是需要回收的垃圾呢?
办法就是:让数组所有元素的zval容器的refcount减一。如果最终数组指向的zval的refcount值为0,那么此数组就是垃圾。正常情况下,最终的refcount应该为1。
挺难理解的吧,看图:
花荣的说明:图中的a符号,我也不知道是怎样来的。。。。。。反正它不是原来的数组a了。
就假设它是另外一个新的变量好了,它指向array(0),因此array(0)的zval的refcount从1变成了2。
另外,所有zval的默认颜色都是黑色。
如果每当refcount减少的时候,都去完成一次垃圾回收过程,未免效率不高。
此算法准备了一个root buffer(也就是一个缓冲区),当一个zval容器的refcount减少到一个非零值的时候,
就把这个zval扔到root buffer里面,然后标记此zval为粉色。
同时还要保证,每个zval只会被扔进来一次~~。
使用了颜色之后,确实方便,只扔黑色的zval进来就可以了。
只有当root buffer被充满的时候(大概需要一万个zval容器才能充满它),
垃圾回收才开始启动。如上图中的A步骤所示。
在上图中的B步骤里,垃圾回收开始启动,
对于root buffer中的每一个zval,
都要使用深度优先的搜索算法(depth-first search),寻找它的所有元素的zval(在这个例子里面,就是数组的所有元素),
并且把元素的zval的refcount减一,然后标记被减一的zval为灰色。
在C步骤里面,依然是对于root buffer中的每一个zval,
都使用深度优先的搜索算法,寻找它的所有元素的zval。
如果元素的zval的refcount是0,就把这个zval标记为白色(在上图中以蓝色表示)。
如果元素的zval的refcount大于0,就让它以及它的所有子元素的zval的refcount加一,恢复原值,标记为黑色。这一步使用的仍然是深度优先算法。
步骤D,清空root buffer。同时把所有标记为白色的zval删除掉,释放空间。
PS:不知道我讲明白没有,反正当时我是看了好久才明白过来。
在此感谢 http://blog.csdn.net/phpkernel/archive/2010/07/14/5734743.aspx 此文作者。他从源代码的角度对GC进行了分析。有一定的可取之处。
结合原来的数组a,我们再来分析一下这个过程。
为了方便,我们把数组的zval命名为zval_array_a。把数组元素0的zval命名为zval_array_0。
unset($a)之后,
数组a指向的zval的refcount减一,值为1,于是它就被扔到了root buffer中,被标记为粉色。
这就是Step A。 Step A里面的符号a,仍然假设它是一个其它变量好了。。与原来的数组a没有任何关系。
假设这个时候,root buffer满了,垃圾回收开始启动。
进入步骤B。对root buffer中的每个zval进行遍历,
假设遍历到zval_array_a了。
使用深度优先算法,寻找zval_array_a的所有元素的zval,
找到了二个zval,一个是zval_array_0,另一个是zval_array_a。
zval_array_0的refcount是2, zval_array_a的refcount是1。
对其refcount进行减一,
zval_array_0的refcount变为1, zval_array_a的refcount变为0。
最后把zval_array_a和zval_array_0标记为灰色。步骤B完成。
进入步骤C,对root buffer中的每个zval进行遍历,
假设遍历到zval_array_a了。
使用深度优先算法,寻找zval_array_a的所有元素的zval,
找到了二个zval,一个是zval_array_0,另一个是zval_array_a。
元素zval_array_a的refcount为0,标记它为白色(图上的蓝色部分),
元素zval_array_0的refcount大于0,
标记为黑色,并且让它的refcount加一。
开始采用深度优先算法,寻找zval_array_0的子元素,如果能找到就给他们搞成黑色,再把他们的refcount加一。没找到就算了。
步骤C结束。
进入步骤D,root buffer被清空。同时,白色的zval—-zval_array_a被删除掉。
垃圾回收完成。
对于一个包含环形引用的数组,对数组中包含的每个元素的zval进行减1操作,之后如果发现数组自身的zval的refcount变成了0,那么可以判断这个数组是一个垃圾。
默认情况下,php 5.3中的垃圾回收是被启用的。可以在php.ini中对其进行设置:zend.enable_gc。
关闭GC之后,php仍然会把zval扔到root buffer中,但不会对其进行分析和处理,
如果root buffer满了,新的zval将不会再进入root buffer中。
The reason why possible roots are recorded even if the mechanism has been disable is because it’s faster to record possible roots, than to have to check whether the mechanism is turned on every time a possible root could be found。
之所以在GC关闭的时候,还会把zval扔到root buffer中,是因为这种操作速度很快,
比每次refcount-1的时候都去检测GC是否开启还要快一些。
由于垃圾回收的整个过程还是比较消耗时间的,如果我们的部分程序对时间很敏感,
那么有必要在程序中实时地关闭垃圾回收,PHP的开发者也想到了这一点:
除了在php.ini中对其设置,我们还可以在PHP代码里面使用gc_enable()和gc_disable()来开启/关闭垃圾回收。
通过gc_collect_cycles()函数,可以强制进行一次垃圾回收,不管root buffer有没有满。
在gc_disable()之前,最好先去调用一下gc_collect_cycles(),
把可回收的垃圾都回收一下。
在这篇文章里面,我们讲了garbage collection 垃圾回收是怎样运作的,以及在PHP中如何控制垃圾回收,
在下一篇文章里面,我们会进行一些评测Benchmark,来看一下GC的性能如何。