主页 > PHP教程 > 正文

浅谈PHP5中废物收回算法(Garbage Collection)的演化

前语

PHP是一门保管型言语,在PHP编程中程序员不需求手艺处理内存资源的分配与开释(运用C编写PHP或Zend扩展在外),这就意味着PHP自身完成了废物收回机制(Garbage Collection)。现在假如去PHP官方网站(php.net)能够看到,现在PHP5的两个分支版别PHP5.2和PHP5.3是别离更新的,这是因为许多项目依然运用5.2版别的PHP,而5.3版别对5.2并不是彻底兼容。PHP5.3在PHP5.2的根底上做了许多改善,其间废物收回算法就归于一个比较大的改动。本文将别离评论PHP5.2和PHP5.3的废物收回机制,并评论这种演化和改善关于程序员编写PHP的影响以及要留意的问题。

PHP变量及相关内存目标的内部表明

废物收回说究竟是对变量及其所相关内存目标的操作,所以在评论PHP的废物收回机制之前,先扼要介绍PHP中变量及其内存目标的内部表明(其C源代码中的表明)。

PHP官方文档中将PHP中的变量划分为两类:标量类型和杂乱类型。标量类型包含布尔型、整型、浮点型和字符串;杂乱类型包含数组、目标和资源;还有一个NULL比较特别,它不划分为任何类型,而是独自成为一类。

一切这些类型,在PHP内部统一用一个叫做zval的结构表明,在PHP源代码中这个结构名称为“_zval_struct”。zval的详细界说在PHP源代码的“Zend/zend.h”文件中,下面是相关代码的摘抄。

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中只需5个字段,可是PHP中算上NULL有8种数据类型,那么PHP内部是怎么用5个字段表明8种类型呢?这算是PHP规划比较奇妙的一个当地,它经过复用字段达到了削减字段的意图。例如,在PHP内部布尔型、整型及资源(只需存储资源的标识符即可)都是经过lval字段存储的;dval用于存储浮点型;str存储字符串;ht存储数组(留意PHP中的数组其实是哈希表);而obj存储目标类型;假如一切字段悉数置为0或NULL则表明PHP中的NULL,这样就达到了用5个字段存储8种类型的值。

而当时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的“Write-On-Copy”机制有关,因为这个论题不是本文要点,因而这儿不再胪陈,读者只需记住refcount__gc这个字段的效果即可。

PHP5.2中的废物收回算法——Reference Counting

PHP5.2中运用的内存收回算法是大名鼎鼎的Reference Counting,这个算法中文翻译叫做“引证计数”,其思维十分直观和简练:为每个内存目标分配一个计数器,当一个内存目标树立时计数器初始化为1(因而此刻总是有一个变量引证此目标),今后每有一个新变量引证此内存目标,则计数器加1,而每逢削减一个引证此内存目标的变量则计数器减1,当废物收回机制运作的时分,将一切计数器为0的内存目标毁掉并收回其占用的内存。而PHP中内存目标便是zval,而计数器便是refcount__gc。

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

<?php
 
$val1 = 100; //zval(val1).refcount_gc = 1;
$val2 = $val1; //zval(val1).refcount_gc = 2,zval(val2).refcount_gc = 2(因为是Write on copy,当时val2与val1一起引证一个zval)
$val2 = 200; //zval(val1).refcount_gc = 1,zval(val2).refcount_gc = 1(此处val2新建了一个zval)
unset($val1); //zval(val1).refcount_gc = 0($val1引证的zval再也不可用,会被GC收回)
 
?>
Reference Counting简略直观,完成便利,但却存在一个丧命的缺点,便是简略形成内存走漏。许多朋友或许现已认识到了,假如存在循环引证,那么Reference Counting就或许导致内存走漏。例如下面的代码:
<?php
$a = array();
$a[] = & $a;
unset($a);
 
?>

这段代码首要树立了数组a,然后让a的第一个元素按引证指向a,这时a的zval的refcount就变为2,然后咱们毁掉变量a,此刻a开端指向的zval的refcount为1,可是咱们再也没有办法对其进行操作,因为其形成了一个循环自引证,如下图所示:

其间灰色部分表明现已不复存在。因为a之前指向的zval的refcount为1(被其HashTable的第一个元素引证),这个zval就不会被GC毁掉,这部分内存就走漏了。

这儿特别要指出的是,PHP是经过符号表(Symbol Table)存储变量符号的,大局有一个符号表,而每个杂乱类型如数组或目标有自己的符号表,因而上面代码中,a和a[0]是两个符号,可是a贮存在大局符号表中,而a[0]贮存在数组自身的符号表中,且这儿a和a[0]引证同一个zval(当然符号a后来被毁掉了)。期望读者朋友留意辨明符号(Symbol)的zval的联系。

在PHP只用于做动态页面脚本时,这种走漏或许不是很要紧,因为动态页面脚本的生命周期很短,PHP会确保当脚本履行结束后,开释其一切资源。可是PHP发展到现在现已不仅仅用作动态页面脚本这么简略,假如将PHP用在生命周期较长的场景中,例如主动化测验脚本或deamon进程,那么经过屡次循环后堆集下来的内存走漏或许就会很严重。这并不是我在耸人听闻,我从前实习过的一个公司就经过PHP写的deamon进程来与数据存储服务器交互。

因为Reference Counting的这个缺点,PHP5.3改善了废物收回算法。

PHP5.3中的废物收回算法——Concurrent Cycle Collection in Reference Counted Systems

PHP5.3的废物收回算法依然以引证计数为根底,可是不再是运用简略计数作为收回原则,而是运用了一种同步收回算法,这个算法由IBM的工程师在论文Concurrent Cycle Collection in Reference Counted Systems中提出。

这个算法可谓适当杂乱,从论文29页的数量我想咱们也能看出来,所以我不计划(也没有才能)完好论说此算法,有爱好的朋友能够阅览上面的说到的论文(强烈推荐,这篇论文十分精彩)。

我在这儿,只能大体描述一下此算法的基本思维。

首要PHP会分配一个固定巨细的“根缓冲区”,这个缓冲区用于寄存固定数量的zval,这个数量默许是10,000,假如需求修正则需求修正源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后从头编译。

由上文咱们能够知道,一个zval假如有引证,要么被大局符号表中的符号引证,要么被其它表明杂乱类型的zval中的符号引证。因而在zval中存在一些或许根(root)。这儿咱们暂且不评论PHP是怎么发现这些或许根的,这是个很杂乱的问题,总归PHP有办法发现这些或许根zval并将它们投入根缓冲区。

当根缓冲区满额时,PHP就会履行废物收回,此收回算法如下:

1、对每个根缓冲区中的根zval依照深度优先遍历算法遍历一切能遍历到的zval,并将每个zval的refcount减1,一起为了防止对同一zval屡次减1(因为或许不同的根能遍历到同一个zval),每次对某个zval减1后就对其标记为“已减”。

2、再次对每个缓冲区中的根zval深度优先遍历,假如某个zval的refcount不为0,则对其加1,不然坚持其为0。

3、清空根缓冲区中的一切根(留意是把这些zval从缓冲区中铲除而不是毁掉它们),然后毁掉一切refcount为0的zval,并收回其内存。

假如不能彻底了解也没有联系,只需记住PHP5.3的废物收回算法有以下几点特性:

1、并不是每次refcount削减时都进入收回周期,只需根缓冲区满额后在开端废物收回。

2、能够处理循环引证问题。

3、能够总将内存走漏坚持在一个阈值以下。

PHP5.2与PHP5.3废物收回算法的功能比较

因为我现在条件所限,我就不从头规划实验了,而是直接引证PHP Manual中的实验,关于两者的功能比较请参阅PHP Manual中的相关章节:http://www.php.net/manual/en/features.gc.performance-considerations.php。

首要是内存走漏实验,下面直接引证PHP Manual中的实验代码和实验成果图:

<?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";
    }
}
?>

PHP内存走漏实验

能够看到在或许引发累积性内存走漏的场景下,PHP5.2发作继续累积性内存走漏,而PHP5.3则总能将内存走漏控制在一个阈值以下(与根缓冲区巨细有关)。

别的是关于功能方面的比照:

<?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";
?>

这个脚本履行1000000次循环,使得延迟时刻满足进行比照。

然后运用CLI办法别离在翻开内存收回和封闭内存收回的的情况下运转此脚本:

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

在我的机器环境下,运转时刻别离为6.4s和7.2s,能够看到PHP5.3的废物收回机制会慢一些,可是影响并不大。

与废物收回算法相关的PHP装备

能够经过修正php.ini中的zend.enable_gc来翻开或封闭PHP的废物收回机制,也能够经过调用gc_enable()或gc_disable()翻开或封闭PHP的废物收回机制。在PHP5.3中即便封闭了废物收回机制,PHP依然会记载或许根到根缓冲区,仅仅当根缓冲区满额时,PHP不会主动运转废物收回,当然,任何时分您都能够经过手艺调用gc_collect_cycles()函数强制履行内存收回。


上一篇:11个发问频率最高的PHP面试题
下一篇:跨站恳求假造CSRF攻防

PythonTab微信大众号:

image

Python技能交流合作群 ( 请勿加多个群 ):

群1: 87464755

群2: 333646237

群3: 318130924

群4: 385100854