在5.2及更早版本的PHP中,没有专门的垃圾回收器GC(Garbage Collection),引擎在判断一个变量空间是否可以被释放的时候是依据这个变量的zval的refcount的值,若是refcount为0,那么变量的空间能够被释放,不然就不释放,这是一种很是简单的GC实现。然而在这种简单的GC实现方案中,出现了意想不到的变量内存泄漏状况(Bug:http://bugs.php.net/bug.php?id=33595),引擎将没法回收这些内存,因而在PHP5.3中出现了新的GC,新的GC有专门的机制负责清理垃圾数据,防止内存泄漏。本文将详细的阐述PHP5.3中新的GC运行机制。php
目前不多有详细的资料介绍新的GC,本文将是目前国内最为详细的从源码角度介绍PHP5.3中GC原理的文章。其中关于垃圾产生以及算法简介部分由笔者根据手册翻译而来,固然其中融入了本人的一些见解。手册中相关内容:Garbage Collection算法
首先咱们须要定义一下“垃圾”的概念,新的GC负责清理的垃圾是指变量的容器zval还存在,可是又没有任何变量名指向此zval。所以GC判断是否为垃圾的一个重要标准是有没有变量名指向变量容器zval。数组
假设咱们有一段PHP代码,使用了一个临时变量$tmp存储了一个字符串,在处理完字符串以后,就不须要这个$tmp变量了,$tmp变量对于咱们来讲能够算是一个“垃圾”了,可是对于GC来讲,$tmp其实并非一个垃圾,$tmp变量对咱们没有意义,可是这个变量实际还存在,$tmp符号依然指向它所对应的zval,GC会认为PHP代码中可能还会使用到此变量,因此不会将其定义为垃圾。函数
那么若是咱们在PHP代码中使用完$tmp后,调用unset删除这个变量,那么$tmp是否是就成为一个垃圾了呢。很惋惜,GC仍然不认为$tmp是一个垃圾,由于$tmp在unset以后,refcount减小1变成了0(这里假设没有别的变量和$tmp指向相同的zval),这个时候GC会直接将$tmp对应的zval的内存空间释放,$tmp和其对应的zval就根本不存在了。此时的$tmp也不是新的GC所要对付的那种“垃圾”。那么新的GC究竟要对付什么样的垃圾呢,下面咱们将生产一个这样的垃圾。 性能
若是读者已经阅读了变量内部存储相关的内容,想必对refcount和isref这些变量内部的信息有了必定的了解。这里咱们将结合手册中的一个例子来介绍垃圾的产生过程:测试
<?php $a = "new string"; ?>
在这么简单的一个代码中,$a变量内部存储信息为:a: (refcount=1, is_ref=0)='new string'spa
当把$a赋值给另一个变量的时候,$a对应的zval的refcount会加1。.net
<?php $a = "new string"; $b = $a; ?>
此时$a和$b变量对应的内部存储信息为 a,b: (refcount=2, is_ref=0)='new string'翻译
当咱们用unset删除$b变量的时候,$b对应的zval的refcount会减小1code
<?php $a = "new string"; //a: (refcount=1, is_ref=0)='new string' $b = $a; //a,b: (refcount=2, is_ref=0)='new string' unset($b); //a: (refcount=1, is_ref=0)='new string' ?>
对于普通的变量来讲,这一切彷佛很正常,可是在复合类型变量(数组和对象)中,会发生比较有意思的事情:
<?php $a = array('meaning' => 'life', 'number' => 42); ?>
a的内部存储信息为:
a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42 )
数组变量自己($a)在引擎内部其实是一个哈希表,这张表中有两个zval项 meaning和number,因此实际上那一行代码中一共生成了3个zval,这3个zval都遵循变量的引用和计数原则,用图来表示:
下面在$a中添加一个元素,并将现有的一个元素的值赋给新的元素:
<?php $a = array('meaning' => 'life', 'number' => 42); $a['life'] = $a['meaning']; ?>
那么$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' )
其中的meaning元素和life元素之指向同一个zval的:
如今,若是咱们试一下,将数组的引用赋值给数组中的一个元素,有意思的事情就发生了:
<?php $a = array('one'); $a[] = &$a; ?>
这样$a数组就有两个元素,一个索引为0,值为字符one,另一个索引为1,为$a自身的引用,内部存储以下:
a: (refcount=2, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=... )
“...”表示1指向a自身,是一个环形引用:
这个时候咱们对$a进行unset,那么$a会从符号表中删除,同时$a指向的zval的refcount减小1
<?php $a = array('one'); $a[] = &$a; unset($a); ?>
那么问题也就产生了,$a已经不在符号表中了,用户没法再访问此变量,可是$a以前指向的zval的refcount变为1而不是0,所以不能被回收,这样产生了内存泄露:
这样,这么一个zval就成为了一个真是意义的垃圾了,新的GC要作的工做就是清理这种垃圾。
为解决这种垃圾,产生了新的GC。
在PHP5.3版本中,使用了专门GC机制清理垃圾,在以前的版本中是没有专门的GC,那么垃圾产生的时候,没有办法清理,内存就白白浪费掉了。在PHP5.3源代码中多了如下文件:{PHPSRC}/Zend/zend_gc.h {PHPSRC}/Zend/zend_gc.c, 这里就是新的GC的实现,咱们先简单的介绍一下算法思路,而后再从源码的角度详细介绍引擎中如何实现这个算法的。
在较新的PHP手册中有简单的介绍新的GC使用的垃圾清理算法,这个算法名为 Concurrent Cycle Collection in Reference Counted Systems , 这里不详细介绍此算法,根据手册中的内容来先简单的介绍一下思路:
首先咱们有几个基本的准则:
若是一个zval的refcount增长,那么此zval还在使用,不属于垃圾
若是一个zval的refcount减小到0, 那么zval能够被释放掉,不属于垃圾
若是一个zval的refcount减小以后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾
只有在准则3下,GC才会把zval收集起来,而后经过新的算法来判断此zval是否为垃圾。那么如何判断这么一个变量是否为真正的垃圾呢?
简单的说,就是对此zval中的每一个元素进行一次refcount减1操做,操做完成以后,若是zval的refcount=0,那么这个zval就是一个垃圾。这个原理咋看起来很简单,可是又不是那么容易理解,起初笔者也没法理解其含义,直到挖掘了源代码以后才算是了解。若是你如今不理解没有关系,后面会详细介绍,这里先把这算法的几个步骤描叙一下,首先引用手册中的一张图:
A:为了不每次变量的refcount减小的时候都调用GC的算法进行垃圾判断,此算法会先把全部前面准则3状况下的zval节点放入一个节点(root)缓冲区(root buffer),而且将这些zval节点标记成紫色,同时算法必须确保每个zval节点在缓冲区中之出现一次。当缓冲区被节点塞满的时候,GC才开始开始对缓冲区中的zval节点进行垃圾判断。
B:当缓冲区满了以后,算法以深度优先对每个节点所包含的zval进行减1操做,为了确保不会对同一个zval的refcount重复执行减1操做,一旦zval的refcount减1以后会将zval标记成灰色。须要强调的是,这个步骤中,起初节点zval自己不作减1操做,可是若是节点zval中包含的zval又指向了节点zval(环形引用),那么这个时候须要对节点zval进行减1操做。
C:算法再次以深度优先判断每个节点包含的zval的值,若是zval的refcount等于0,那么将其标记成白色(表明垃圾),若是zval的refcount大于0,那么将对此zval以及其包含的zval进行refcount加1操做,这个是对非垃圾的还原操做,同时将这些zval的颜色变成黑色(zval的默认颜色属性)。
D:遍历zval节点,将C中标记成白色的节点zval释放掉。
这ABCD四个过程是手册中对这个算法的介绍,这还不是那么容易理解其中的原理,这个算法究竟是个什么意思呢?我本身的理解是这样的:
好比仍是前面那个变成垃圾的数组$a对应的zval,命名为zval_a, 若是没有执行unset, zval_a的refcount为2,分别由$a和$a中的索引1指向这个zval。 用算法对这个数组中的全部元素(索引0和索引1)的zval的refcount进行减1操做,因为索引1对应的就是zval_a,因此这个时候zval_a的refcount应该变成了1,这样zval_a就不是一个垃圾。若是执行了unset操做,zval_a的refcount就是1,由zval_a中的索引1指向zval_a,用算法对数组中的全部元素(索引0和索引1)的zval的refcount进行减1操做,这样zval_a的refcount就会变成0,因而就发现zval_a是一个垃圾了。 算法就这样发现了顽固的垃圾数据。
举了这个例子,读者大概应该可以知道其中的端倪:
对于一个包含环形引用的数组,对数组中包含的每一个元素的zval进行减1操做,以后若是发现数组自身的zval的refcount变成了0,那么能够判断这个数组是一个垃圾。
这个道理其实很简单,假设数组a的refcount等于m, a中有n个元素又指向a,若是m等于n,那么算法的结果是m减n,m-n=0,那么a就是垃圾,若是m>n,那么算法的结果m-n>0,因此a就不是垃圾了。
m=n表明什么? 表明a的refcount都来自数组a自身包含的zval元素,表明a以外没有任何变量指向它,表明用户代码空间中没法再访问到a所对应的zval,表明a是泄漏的内存,所以GC将a这个垃圾回收了。
在PHP中,GC默认是开启的,你能够经过ini文件中的 zend.enable_gc 项来开启或则关闭GC。当GC开启的时候,垃圾分析算法将在节点缓冲区(roots buffer)满了以后启动。缓冲区默承认以放10,000个节点,固然你也能够经过修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 来改变这个数值,须要从新编译连接PHP。当GC关闭的时候,垃圾分析算法就不会运行,可是相关节点还会被放入节点缓冲区,这个时候若是缓冲区节点已经放满,那么新的节点就不会被记录下来,这些没有被记录下来的节点就永远也不会被垃圾分析算法分析。若是这些节点中有循环引用,那么有可能产生内存泄漏。之因此在GC关闭的时候还要记录这些节点,是由于简单的记录这些节点比在每次产生节点的时候判断GC是否开启更快,另外GC是能够在脚本运行中开启的,因此记录下这些节点,在代码运行的某个时候若是又开启了GC,这些节点就能被分析算法分析。固然垃圾分析算法是一个比较耗时的操做。
在PHP代码中咱们能够经过gc_enable()和gc_disable()函数来开启和关闭GC,也能够经过调用gc_collect_cycles()在节点缓冲区未满的状况下强制执行垃圾分析算法。这样用户就能够在程序的某些部分关闭或则开启GC,也可强制进行垃圾分析算法。
1. 防止泄漏节省内存
新的GC算法的目的就是为了防止循环引用的变量引发的内存泄漏问题,在PHP中GC算法,当节点缓冲区满了以后,垃圾分析算法会启动,而且会释放掉发现的垃圾,从而回收内存,在PHP手册上给了一段代码和内存使用情况图:
<?php class Foo { public $var = '3.1415962654'; } $baseMemory = memory_get_usage(); for ( $i = 0; $i <= 100000; $i++ ) { $a = new Foo; $a->self = $a; if ( $i % 500 === 0 ) { echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "/n"; } } ?>
这段代码的循环体中,新建了一个对象变量,而且用对象的一个成员指向了本身,这样就造成了一个循环引用,当进入下一次循环的时候,又一次给对象变量从新赋值,这样会致使以前的对象变量内存泄漏,在这个例子里面有两个变量泄漏了,一个是对象自己,另一个是对象中的成员self,可是这两个变量只有对象会做为垃圾收集器的节点被放入缓冲区(由于从新赋值至关于对它进行了unset操做,知足前面的准则3)。在这里咱们进行了100,000次循环,而GC在缓冲区中有10,000节点的时候会启动垃圾分析算法,因此这里一共会进行10次的垃圾分析算法。从图中能够清晰的看到,在5.3版本PHP中,每次GC的垃圾分析算法被触发后,内存会有一个明显的减小。而在5.2版本的PHP中,内存使用量会一直增长。
2. 运行效率影响
启用了新的GC后,垃圾分析算法将是一个比较耗时的操做,手册中给了一段测试代码:
<?php class Foo { public $var = '3.1415962654'; } for ( $i = 0; $i <= 1000000; $i++ ) { $a = new Foo; $a->self = $a; } echo memory_get_peak_usage(), "/n"; ?>
而后分别在GC开启和关闭的状况下执行这段代码:
time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php # and time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php
最终在该机器上,第一次执行大概使用10.7秒,第二次执行大概使用11.4秒,性能大约下降7%,不过内存的使用量下降了98%,从931M下降到了10M。固然这并非一个比较科学的测试方法,可是也能说明必定的问题。这种代码测试的是一种极端恶劣条件,实际代码中,特别是在WEB的应用中,很难出现大量循环引用,GC的分析算法的启动不会这么频繁,小规模的代码中甚至不多有机会启动GC分析算法。
总结:
当GC的垃圾分析算法执行的时候,PHP脚本的效率会受到必定的影响,可是小规模的代码通常不会有这个机会运行这个算法。若是一旦脚本中GC分析算法开始运行了,那么将花费少许的时间节省出来了大量的内存,是一件很是划算的事情。新的GC对一些长期运行的PHP脚本效果更好,好比PHP的DAEMON守护进程,或则PHP-GTK进程等等。