php

位置:IT落伍者 >> php >> 浏览文章

浅谈PHP5中垃圾回收算法(Garbage Collection)的演化


发布日期:2018年10月08日
 
浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

前言PHP是一门托管型语言在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外)这就意味着PHP本身实现了垃圾回收机制(Garbage Collection)现在如果去PHP官方网站(phpnet)可以看到目前PHP的两个分支版本PHP和PHP是分别更新的这是因为许多项目仍然使用版本的PHP版本对并不是完全兼容PHP在PHP的基础上做了诸多改进其中垃圾回收算法就属于一个比较大的改变本文将分别讨论PHP和PHP的垃圾回收机制并讨论这种演化和改进对于程序员编写PHP的影响以及要注意的问题

PHP变量及关联内存对象的内部表示

垃圾回收说到底是对变量及其所关联内存对象的操作所以在讨论PHP的垃圾回收机制之前先简要介绍PHP中变量及其内存对象的内部表示(其C源代码中的表示)

PHP官方文档中将PHP中的变量划分为两类标量类型和复杂类型标量类型包括布尔型整型浮点型和字符串复杂类型包括数组对象和资源还有一个NULL比较特殊它不划分为任何类型而是单独成为一类

所有这些类型在PHP内部统一用一个叫做zval的结构表示在PHP源代码中这个结构名称为“_zval_struct”zval的具体定义在PHP源代码的“Zend/zendh”文件中下面是相关代码的摘录

                                                

typedef union _zvalue_value {long lval;/* long value */double dval;/* double value */struct {char *val;int len;} str;HashTable *ht;/* hash table value */zend_object_value obj;} zvalue_value;struct _zval_struct {/* Variable information */zvalue_value value;/* value */zend_uint refcount__gc;zend_uchar type;/* active type */zend_uchar is_ref__gc;};

                        

其中联合体“_zvalue_value”用于表示PHP中所有变量的值这里之所以使用union是因为一个zval在一个时刻只能表示一种类型的变量可以看到_zvalue_value中只有个字段但是PHP中算上NULL有种数据类型那么PHP内部是如何用个字段表示种类型呢?这算是PHP设计比较巧妙的一个地方它通过复用字段达到了减少字段的目的例如在PHP内部布尔型整型及资源(只要存储资源的标识符即可)都是通过lval字段存储的dval用于存储浮点型str存储字符串ht存储数组(注意PHP中的数组其实是哈希表)而obj存储对象类型如果所有字段全部置为或NULL则表示PHP中的NULL这样就达到了用个字段存储种类型的值

而当前zval中的value(value的类型即是_zvalue_value)到底表示那种类型则由“_zval_struct”中的type确定_zval_struct即是zval在C语言中的具体实现每个zval表示一个变量的内存对象除了value和type可以看到_zval_struct中还有两个字段refcount__gc和is_ref__gc从其后缀就可以断定这两个家伙与垃圾回收有关没错PHP的垃圾回收全靠这俩字段了其中refcount__gc表示当前有几个变量引用此zval而is_ref__gc表示当前zval是否被按引用引用这话听起来很拗口这和PHP中zval的“WriteOnCopy”机制有关由于这个话题不是本文重点因此这里不再详述读者只需记住refcount__gc这个字段的作用即可

PHP中的垃圾回收算法——Reference Counting

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

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

                                    

<?php
            
            $val = ; //zval(val)refcount_gc = ;
            $val = $val; //zval(val)refcount_gc = zval(val)refcount_gc = (因为是Write on copy当前val与val共同引用一个zval)
            $val = ; //zval(val)refcount_gc = zval(val)refcount_gc = (此处val新建了一个zval)
            unset($val); //zval(val)refcount_gc = ($val引用的zval再也不可用会被GC回收)
            
            ?>

                        

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

                                    

<?php
            
            $a = array();
            $a[] = & $a;
            unset($a);
            
            ?>

                        

这段代码首先建立了数组a然后让a的第一个元素按引用指向a这时a的zval的refcount就变为然后我们销毁变量a此时a最初指向的zval的refcount为但是我们再也没有办法对其进行操作因为其形成了一个循环自引用如下图所示

其中灰色部分表示已经不复存在由于a之前指向的zval的refcount为(被其HashTable的第一个元素引用)这个zval就不会被GC销毁这部分内存就洩露了

这里特别要指出的是PHP是通过符号表(Symbol Table)存储变量符号的全局有一个符号表而每个复杂类型如数组或对象有自己的符号表因此上面代码中a和a[]是两个符号但是a储存在全局符号表中而a[]储存在数组本身的符号表中且这里a和a[]引用同一个zval(当然符号a后来被销毁了)希望读者朋友注意分清符号(Symbol)的zval的关系

在PHP只用于做动态页面脚本时这种洩露也许不是很要紧因为动态页面脚本的生命周期很短PHP会保证当脚本执行完毕后释放其所有资源但是PHP发展到目前已经不仅仅用作动态页面脚本这么简单如果将PHP用在生命周期较长的场景中例如自动化测试脚本或deamon进程那么经过多次循环后积累下来的内存洩露可能就会很严重这并不是我在耸人听闻我曾经实习过的一个公司就通过PHP写的deamon进程来与数据存储服务器交互

由于Reference Counting的这个缺陷PHP改进了垃圾回收算法

PHP中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems

PHP的垃圾回收算法仍然以引用计数为基础但是不再是使用简单计数作为回收准则而是使用了一种同步回收算法这个算法由IBM的工程师在论文Concurrent Cycle Collection in Reference Counted Systems中提出

这个算法可谓相当复杂从论文页的数量我想大家也能看出来所以我不打算(也没有能力)完整论述此算法有兴趣的朋友可以阅读上面的提到的论文(强烈推荐这篇论文非常精彩)

我在这里只能大体描述一下此算法的基本思想

首先PHP会分配一个固定大小的“根缓沖区”这个缓沖区用于存放固定数量的zval这个数量默认是如果需要修改则需要修改源代码Zend/zend_gcc中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新编译

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

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

对每个根缓沖区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval并将每个zval的refcount减同时为了避免对同一zval多次减(因为可能不同的根能遍历到同一个zval)每次对某个zval减后就对其标记为“已减”

再次对每个缓沖区中的根zval深度优先遍历如果某个zval的refcount不为则对其加否则保持其为

清空根缓沖区中的所有根(注意是把这些zval从缓沖区中清除而不是销毁它们)然后销毁所有refcount为的zval并收回其内存

如果不能完全理解也没有关系只需记住PHP的垃圾回收算法有以下几点特性

并不是每次refcount减少时都进入回收周期只有根缓沖区满额后在开始垃圾回收

可以解决循环引用问题

可以总将内存洩露保持在一个阈值以下

PHP与PHP垃圾回收算法的性能比较

由于我目前条件所限我就不重新设计试验了而是直接引用PHP Manual中的实验关于两者的性能比较请参考PHP Manual中的相关章节

首先是内存洩露试验下面直接引用PHP Manual中的实验代码和试验结果图

                                    

<?php
            class Foo
            {
             public $var = ;
            }
            
            $baseMemory = memory_get_usage();
            
            for ( $i = ; $i <= ; $i++ )
            {
             $a = new Foo;
             $a>self = $a;
             if ( $i % === )
             {
             echo sprintf( %d: $i ) memory_get_usage() $baseMemory "n";
             }
            }
            ?>

                        

可以看到在可能引发累积性内存洩露的场景下PHP发生持续累积性内存洩露而PHP则总能将内存洩露控制在一个阈值以下(与根缓沖区大小有关)

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

                                    

<?php
            class Foo
            {
             public $var = ;
            }
            
            for ( $i = ; $i <= ; $i++ )
            {
             $a = new Foo;
             $a>self = $a;
            }
            
            echo memory_get_peak_usage() "n";
            ?>

                        

这个脚本执行次循环使得延迟时间足够进行对比然后使用CLI方式分别在打开内存回收和关闭内存回收的的情况下运行此脚本

                                    

time php dzendenable_gc= dmemory_limit= n examplephp
            # and
            time php dzendenable_gc= dmemory_limit= n examplephp

                        

在我的机器环境下运行时间分别为s和s可以看到PHP的垃圾回收机制会慢一些但是影响并不大

与垃圾回收算法相关的PHP配置

可以通过修改phpini中的zendenable_gc来打开或关闭PHP的垃圾回收机制也可以通过调用gc_enable( )或gc_disable( )打开或关闭PHP的垃圾回收机制在PHP中即使关闭了垃圾回收机制PHP仍然会记录可能根到根缓沖区只是当根缓沖区满额时PHP不会自动运行垃圾回收当然任何时候您都可以通过手工调用gc_collect_cycles( )函数强制执行内存回收

               

上一篇:php函数range() round()和list()如何使用

下一篇:PHP中使用pcntl和libevent实现Timer功能