名扬数据:对于PHP 5中的垃圾回收算法的演化

可以通过调用gc_enabl或gc_disabl打开或关闭PHP垃圾回收机制。PHP5.3中即使关闭了垃圾回收机制,可以通过修改php.ini中的zend.enable_gc来打开或关闭PHP垃圾回收机制。PHP仍然会记录可能根到根缓冲区,只是当根缓冲区满额时,PHP不会自动运行垃圾回收,当然,任何时候您都可以通过手工调用gc_collect_cycl函数强制执行内存回收。PHP编程中程序员不需要手工处置内存资源的分配与释放(使用C编写PHP或Zend扩展除外)这就意味着PHP自身实现了垃圾回收机制(GarbagCollect现在如果去PHP官方网站(php.net可以看到目前PHP5两个分支版本PHP5.2和PHP5.3分别更新的这是因为许多项目仍然使用5.2版本的PHP而5.3版本对5.2并不是完全兼容。PHP5.3PHP5.2基础上做了诸多改进,PHP一门托管型语言。其中垃圾回收算法就属于一个比较大的改变。本文将分别讨论PHP5.2和PHP5.3垃圾回收机制,并讨论这种演化和改进对于顺序员编写PHP影响以及要注意的问题。

PHP变量及关联内存对象的内部表示,所以在讨论PHP垃圾回收机制之前,垃圾回收说到底是对变量及其所关联内存对象的操作。先简要介绍PHP中变量及其内存对象的内部表示(其C源代码中的表示),不划分为任何类型,PHP官方文档中将PHP中的变量划分为两类:标量类型和复杂类型。标量类型包括布尔型、整型、浮点型和字符串;复杂类型包括数组、对象和资源;还有一个NULL比较特殊。而是单独成为一类。PHP内部统一用一个叫做zval结构表示,所有这些类型。PHP源代码中这个结构名称为“_zval_structzval具体定义在PHP源代码的Zend/zend.h文件中,下面是相关代码的摘录。

typedefunion_zvalue_valu{ 

   longlval;                 /*longvalu*/

   doubldval;               /*doublvalu*/

   struct{ 

       char*val; 

       intlen; 

   }str; 

   HashTabl*ht;             /*hashtablvalu*/

   zend_object_valuobj; 

}zvalue_value; 

 

struct_zval_struct{ 

   /*Variablinform*/

   zvalue_valuvalue;      

/*valu*/

   zend_uintrefcount__gc; 

   zend_uchartype;   /*activtype*/

   zend_ucharis_ref__gc; 

};

这里之所以使用union因为一个zval一个时刻只能表示一种类型的变量。可以看到_zvalue_valu中只有5个字段,其中联合体“_zvalue_valu用于表示PHP中所有变量的值。但是PHP中算上NULL有8种数据类型,那么PHP内部是如何用5个字段表示8种类型呢?这算是PHP设计比较巧妙的一个地方,通过复用字段达到减少字段的目的例如,PHP内部布尔型、整型及资源(只要存储资源的标识符即可)都是通过lval字段存储的;dval用于存储浮点型;str存储字符串;ht存储数组(注意PHP中的数组其实是哈希表);而obj存储对象类型;如果所有字段全部置为0或NULL则表示PHP中的NULL这样就达到用5个字段存储8种类型的值。则由“_zval_struct中的type确定。_zval_struct即是zvalC语言中的具体实现,而当前zval中的valuvalu类型即是_zvalue_valu底表示那种类型。每个zval表示一个变量的内存对象。除了valu和type可以看到_zval_struct中还有两个字段refcount__gc和is_ref__gc从其后缀就可以断定这两个家伙与垃圾回收有关。没错,PHP垃圾回收全靠这俩字段了其中refcount__gc表示当前有几个变量引用此zval而is_ref__gc表示当前zval否被按引用引用,这话听起来很拗口,这和PHP中zvalWrite-On-Copi机制有关,由于这个话题不是本文重点,因此这里不再详述,读者只需记住refcount__gc这个字段的作用即可。

PHP5.2中的垃圾回收算法—ReferCounting,当一个内存对象建立时计数器初始化为1因此此时总是有一个变量引用此对象)以后每有一个新变量引用此内存对象,PHP5.2中使用的内存回收算法是大名鼎鼎的ReferCount这个算法中文翻译叫做“引用计数”其思想非常直观和简洁:为每个内存对象分配一个计数器。则计数器加1而每当减少一个引用此内存对象的变量则计数器减1当垃圾回收机制运作的时候,将所有计数器为0内存对象销毁并回收其占用的内存。而PHP中内存对象就是zval而计数器就是refcount__gc

例如下面一段PHP代码演示了PHP5.2计数器的工作原理(计数器值通过xdebug得到

<?php 

$val1=100;//zvalval1.refcount_gc=1; 

zvalval2.refcount_gc=2因为是Writeoncopi当前val2与val1共同引用一个zval $val2=$val1;//zvalval1.refcount_gc=2.

zvalval2.refcount_gc=1此处val2新建了一个zval $val2=200;//zvalval1.refcount_gc=1.

会被GC回收) unset$val1;//zvalval1.refcount_gc=0$val1引用的zval再也不可用。 

?>

实现方便,ReferCount简单直观。但却存在一个致命的缺陷,就是容易造成内存泄露。很多朋友可能已经意识到如果存在循环引用,那么ReferCount就可能导致内存泄露。例如下面的代码:

<?php 

$a=arrai; 

$a[]=&$a; 

unset$a; 

?>

因为其形成了一个循环自引用,这段代码首先建立了数组a然后让a第一个元素按引用指向a这时azvalrefcount就变为2然后我销毁变量a此时a最初指向的zvalrefcount为1但是再也没有方法对其进行操作。

浅谈PHP5中垃圾回收算法(GarbagCollect演化,这部分内存就泄露了其中灰色局部表示已经不复存在由于a之前指向的zvalrefcount为1被其HashTabl第一个元素引用)这个zval就不会被GC销毁。而每个复杂类型如数组或对象有自己的符号表,这里特别要指出的PHP通过符号表(SymbolTabl存储变量符号的全局有一个符号表。因此上面代码中,a和a[0]两个符号,但是a贮存在全局符号表中,而a[0]贮存在数组本身的符号表中,且这里a和a[0]引用同一个zval当然符号a后来被销毁了希望读者朋友注意分清符号(Symbolzval关系。

这种泄露也许不是很要紧,PHP只用于做动态页面脚本时。因为动态页面脚本的生命周期很短,PHP会保证当脚本执行完毕后,释放其所有资源。但是PHP发展到目前已经不只仅用作动态页面脚本这么简单,如果将PHP用在生命周期较长的场景中,例如自动化测试脚本或deamon进程,那么经过多次循环后积累下来的内存泄露可能就会很严重。这并不是耸人听闻,曾经实习过的一个公司就通过PHP写的deamon进程来与数据存储服务器交互。PHP5.3改进了垃圾回收算法。由于ReferCount这个缺陷。PHP5.3中的垃圾回收算法—ConcurrCyclCollectinReferCountSystems,但是不再是使用简单计数作为回收准则,PHP5.3垃圾回收算法仍然以引用计数为基础。而是使用了一种同步回收算法,这个算法由IBM工程师在论文ConcurrCyclCollectinReferCountSystem中提出。

从论文29页的数量我想大家也能看出来,这个算法可谓相当复杂。所以我不打算(也没有能力)完整论述此算法,有兴趣的朋友可以阅读上面的提到论文(强烈推荐,这篇论文非常精彩),只能大体描述一下此算法的基本思想,这里000如果需要修改则需要修改源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MA X_ENPIES然后重新编译。首先PHP会分配一个固定大小的根缓冲区”这个缓冲区用于存放固定数量的zval这个数量默认是10.

一个zval如果有引用,由上文我可以知道。要么被全局符号表中的符号引用,要么被其它表示复杂类型的zval中的符号引用。因此在zval中存在一些可能根(root这里我暂且不讨论PHP如何发现这些可能根的这是个很复杂的问题,总之PHP有方法发现这些可能根zval并将它投入根缓冲区。

PHP就会执行垃圾回收,当根缓冲区满额时。此回收算法如下:

1对每个根缓冲区中的根zval依照深度优先遍历算法遍历所有能遍历到zval并将每个zvalrefcount减1同时为了防止对同一zval多次减1因为可能不同的根能遍历到同一个zval每次对某个zval减1后就对其标记为“已减”

如果某个zvalrefcount不为0则对其加1否则坚持其为02再次对每个缓冲区中的根zval深度优先遍历。清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它然后销毁所有refcount为0zval并收回其内存。只需记住PHP5.3垃圾回收算法有以下几点特性:如果不能完全理解也没有关系。只有根缓冲区满额后在开始垃圾回收。1并不是每次refcount减少时都进入回收周期。

可以解决循环引用问题,可以总将内存泄露坚持在一个阈值以下,PHP5.2与PHP5.3垃圾回收算法的性能比拟,就不重新设计试验了而是直接引用PHPManual中的实验,由于我目前条件所限。下面直接引用PHPManual中的实验代码和试验结果图:首先是内存泄露试验。

<?php 

classFoo 

   public$var='3.1415962654'; 

 

$baseMemori=memory_get_usag; 

 

for$i=0;$i<=100000;$i++ 

   $a=newFoo; 

   $a->self=$a; 

   if$i%500===0 

   { 

$i,       echosprintf'%8d:'.memory_get_usag-$baseMemory,"\n"; 

   } 

?>

PHP

PHP5.2发生继续累积性内存泄露,可以看到可能引发累积性内存泄露的场景下。而PHP5.3则总能将内存泄露控制在一个阈值以下(与根缓冲区大小有关)

另外是关于性能方面的对比:

<?php 

classFoo 

   public$var='3.1415962654'; 

 

for$i=0;$i<=1000000;$i++ 

   $a=newFoo; 

   $a->self=$a; 

 

"\n"; echomemory_get_peak_usag.

?>

使得延迟时间足够进行对比。这个脚本执行1000000次循环。

然后使用CLI方式分别在打开内存回收和关闭内存回收的情况下运行此脚本:

timephp-dzend.enable_gc=0-dmemory_limit=-1-nexample2.php 

#and

timephp-dzend.enable_gc=1-dmemory_limit=-1-nexample2.php

运行时间分别为6.4和7.2可以看到PHP5.3垃圾回收机制会慢一些,机器环境下。但是影响并不大,与垃圾回收算法相关的PHP配置。