PHPPHP新的垃圾回收机制:Zend GC详解

概述

    在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运行机制。

   
目前特别少来详细的资料介绍新的GC,本文将凡当下国内最为详实的于源码角度介绍PHP5.3中GC原理的篇章。其中有关垃圾有与算法简介部分是因为作者根据手册翻译而来,当然其中融入了我的一些理念。手册中有关内容:Garbage
Collection

   
在介绍这新的GC之前,读者必须事先了解PHP中变量的内部存储相关文化,请先阅读 变量的里存储:引用和计数 

 

咦算垃圾

    首先我们要定义一下“垃圾”的定义,新的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’

 

当把$a赋值给另外一个变量的早晚,$a对应之zval的refcount会加1

<?php

$a = “new string”;

$b = $a;

?>
这$a和$b变量呼应之内部存储信息呢

a,b: (refcount=2, is_ref=0)=’new string’

当我们为此unset删除$b变量的上,$b对应的zval的refcount会减少1

<?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都按照变量的援和计数原则,用图来表示:

 

PHP 1

 

 

 

 下面在$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 2

 

 

本,如果我们摸索一下,将反复组的援赋值给数组中之一个要素,有意思的作业就发了:

<?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自身,是一个环形引用:

PHP 3

 

这时节咱们对$a进行unset,那么$a会由符号表中删除,同时$a指向的zval的refcount减少1

<?php

$a = array(‘one’);

$a[] = &$a;

unset($a);

?>

那么问题吗就是发生了,$a曾休以符号表中了,用户无法再次看是变量,但是$a之前对的zval的refcount变为1一旦非是0,因此不克于回收,这样产生了内存泄露:

PHP 4

 

这么,这么一个zval就成为了一个算意义的废料了,新的GC要召开的劳作就是清理这种垃圾。

 

为缓解这种垃圾,产生了新的GC

    在PHP5.3版本中,使用了特别GC机制清理废物,在事先的本子中是绝非特意的GC,那么垃圾产生的时节,没有主意清理,内存就白白浪费掉了。在PHP5.3源代码中大多矣以下文件:{PHPSRC}/Zend/zend_gc.h
{PHPSRC}/Zend/zend_gc.c,
这里虽是新的GC的实现,我们先行简单的牵线一下算法思路,然后还起源码的角度详细介绍引擎中哪些实现这个算法的。

 

新的GC算法

    当较新的PHP手册中产生略的介绍新的GC使用的渣清理算法,这个算法名吧 Concurrent
Cycle Collection in Reference Counted Systems ,
这里不详细介绍此算法,根据手册中之始末来先简单的介绍一下思路:

率先我们发出几乎独核心的律:

1:如果一个zval的refcount增加,那么这zval还于使用,不属垃圾

2:如果一个zval的refcount减少到0, 那么zval可以为放掉,不属垃圾

3:如果一个zval的refcount减少事后大于0,那么是zval还免能够被释放,此zval可能变成一个废弃物

 

除非在规则3下蛋,GC才见面把zval收集起来,然后经过新的算法来判定这个zval是否为垃圾。那么哪些判定这样一个变量是否也真的污染源也?

大概的说,就是对之zval中的每个元素进行同样软refcount减1操作,操作就之后,如果zval的refcount=0,那么是zval就是一个污染源。这个规律咋看起老简短,但是还要休是那么爱了解,起初笔者也束手无策清楚其义,直到开了自代码之后才好不容易了解。如果您本无晓没有涉及,后面会详细介绍,这里先将立即算法的几乎独步骤描叙一下,首先引述手册中之均等摆图:

 

 PHP 5

 

 

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的算法

    在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,也只是强制进行垃圾分析算法。 

   

新的GC算法的性能

1.防护泄漏节省内存

   
新的GC算法的目的就是是为以防万一循环引用的变量引起的内存泄漏问题,在PHP中GC算法,当节点缓冲区满了以后,垃圾分析算法会启动,并且会放出掉发现的垃圾堆,从而回收内存,在PHP手册上吃了一如既往段代码和内存以状况图:

 

 

<?phpclass 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后,垃圾分析算法将凡一个较耗时的操作,手册中为了一样截测试代码:

 

 

 

<?phpclass 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# andtime 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进程等等。

 

 

 

引擎内部GC的实现

前面已经介绍了新的GC的基本原理以及性能相关的内容,其中一些都是在手册中有简单介绍了,那么这里我们将从源代码的角度来分析一下PHP如何实现新的GC。

1.zval的变化

在文件Zend/zend_gc.h中,重新定义了分配一个zval结构的宏:

 

 

[cpp] view plain copy

  1. #undef  ALLOC_ZVAL

  2. #define ALLOC_ZVAL(z)                                   /

  3. do {                                                /

  4. (z) = (zval*)emalloc(sizeof(zval_gc_info));     /

  5. GC_ZVAL_INIT(z);                                /

  6. } while (0)

ALLOC_ZVAL的原始定义是在Zend/zend_alloc.h中,原始的定义只是分配一个zval结构的内存空间,然后在新的GC使用后,分配一个zval空间实际上是分配了一个zval_gc_info结构的空间,下面看看zval_gc_info结构定义:

 

[cpp] view plain copy

  1. typedef struct _zval_gc_info {

  2. zval z;

  3. union {

  4. gc_root_buffer       *buffered;

  5. struct _zval_gc_info *next;

  6. } u;

  7. } zval_gc_info;

zval_gc_info这个结构的第一个成员就是一个zval结构,第二个成员是一个联合体u,是一个指向gc_root_buffer的指针和一个指向_zval_gc_info的指针。  第一个成员为zval结构,这就保证了对zval_gc_info类型指针做类型转换后和zval等价。在ALLOC_ZVAL宏中,分配了一个zval_gc_info的空间后,是将空间的指针转换成了(zval *)。这样就相当于分配了一个zval的空间。然后GC_ZVAL_INIT宏会把zval_gc_info中的成员u的buffered字段设置成NULL:

 

[cpp] view plain copy

  1. #define GC_ZVAL_INIT(z) /

  2. ((zval_gc_info*)(z))->u.buffered = NULL

这个u.buffered指针就是用来表示这个zval对应的节点信息指针。

新的GC会为所有的zval分配一个空间存放节点信息指针,只有当zval被GC放入节点缓冲区的时候,节点信息指针才会被指向一个节点信息结构,否则节点信息指针一直是NULL。

具体方式是通过分配一个zval_gc_info结构来实现,这个结构包含了zval和节点信息指针buffered。

 

 

2.节点信息

zval的节点信息指针buffered指向一个gc_root_buffer类型,这个类型的定义如下:

 

[cpp] view plain copy

  1. typedef struct _gc_root_buffer {

  2. struct _gc_root_buffer   *prev;     /* double-linked list               */

  3. struct _gc_root_buffer   *next;

  4. zend_object_handle        handle;   /* must be 0 for zval               */

  5. union {

  6. zval                 *pz;

  7. zend_object_handlers *handlers;

  8. } u;

  9. } gc_root_buffer;

这是一个双链表的节点结构类型,prev和next用来指向前一个节点和后一个节点,handel是和对象相关的,对象类型的变量比较特殊,我们这里不讨论,u是一个联合体,u.pz用来指向这个节点所对应的zval结构。 这样每一个zval结构和zval对应的节点信息互相被关联在一起了:

通过一个zval指针pz找到节点指针: pr = ((zval_gc_info *)pz)->u.buffered

通过一个节点指针pr找到zval指针: pz = pr->u.pz

 

3.为zval设置节点信息以及节点颜色信息

这里GC应用了一些小技巧,先看看下面相关的宏:

 

[cpp] view
plain copy

  1. #define GC_COLOR  0x03  

  2.   

  3. #define GC_BLACK  0x00  

  4. #define GC_WHITE  0x01  

  5. #define GC_GREY   0x02  

  6. #define GC_PURPLE 0x03  

  7.   

  8. #define GC_ADDRESS(v) /  

  9.     ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))  

  10. #define GC_SET_ADDRESS(v, a) /  

  11.     (v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & GC_COLOR) | ((zend_uintptr_t)(a))))  

  12. #define GC_GET_COLOR(v) /  

  13.     (((zend_uintptr_t)(v)) & GC_COLOR)  

  14. #define GC_SET_COLOR(v, c) /  

  15.     (v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & ~GC_COLOR) | (c)))  

  16. #define GC_SET_BLACK(v) /  

  17.     (v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))  

  18. #define GC_SET_PURPLE(v) /  

  19.     (v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) | GC_PURPLE))  

  20.   

  21. #define GC_ZVAL_INIT(z) /  

  22.     ((zval_gc_info*)(z))->u.buffered = NULL  

  23. #define GC_ZVAL_ADDRESS(v) /  

  24.     GC_ADDRESS(((zval_gc_info*)(v))->u.buffered)  

  25. #define GC_ZVAL_SET_ADDRESS(v, a) /  

  26.     GC_SET_ADDRESS(((zval_gc_info*)(v))->u.buffered, (a))  

  27. #define GC_ZVAL_GET_COLOR(v) /  

  28.     GC_GET_COLOR(((zval_gc_info*)(v))->u.buffered)  

  29. #define GC_ZVAL_SET_COLOR(v, c) /  

  30.     GC_SET_COLOR(((zval_gc_info*)(v))->u.buffered, (c))  

  31. #define GC_ZVAL_SET_BLACK(v) /  

  32.     GC_SET_BLACK(((zval_gc_info*)(v))->u.buffered)  

  33. #define GC_ZVAL_SET_PURPLE(v) /  

  34.     GC_SET_PURPLE(((zval_gc_info*)(v))->u.buffered)  

 

其中宏GC_ZVAL_SET_ADDRESS(v, a)是为v这个zval设置节点信息的指针a,这个宏先得到v中的节点信息指针字段u.buffered,然后调用GC_ADDRESS(v,a)宏,将u.buffered字段设置成指针a。

GC_ADDRESS(v, a)宏的功能是将地址a赋给v,但是它的实现很奇怪:

(v) =
((gc_root_buffer*)((((zend_uintptr_t)(v)) & GC_COLOR) |
((zend_uintptr_t)(a))))

 

怎用如此一个复杂的过程,而且设置指针值为何还要拉到GC_COLOR颜色这个宏?

此地虽得预说说节点的颜色信息保存方法。

在前头GC的算法简介中,提到了急需呢节点上色,而实在在我们节点结构gc_root_buffer中并不曾呀一个字段用来标识节点的水彩,这里GC运用了一个不怎么之技术:利用节点指针的没有点儿位来标识颜色属性。可能读者见面发生疑难,用指针中之各项来保存颜色属性,那么设置颜色后,指针不就是变化了吗,那么还能查看找到指针对应之构造也?
这个还确确实实能查看及! 为什么?
这个与malloc分配的内存地址属性有肯定的关系,glib的malloc分配的内存地址都见面发一定的对齐,这个针对齐值也2
*
SIZE_SZ,在不同位的机器及之价是免相同的,但是可以保证的凡分配出来的指针的最低两位肯定是0,然后看看颜色相关的大幅度,GC_COLOR为0x03,
3只需要简单个二进制位就可知保留,所以拿指针的低两各类来保存颜色值是从来不其他问题之,但是以运指针的时候势必要先期将指针最低的蝇头号还原成0,否则指针指向的价值是张冠李戴的。

 

这样我们虽可知理解为什么GC_ADDRESS需要这样复杂了。因为v中的低2各项保存了v的颜料信息,如果一直将a赋给v会覆盖掉颜色信息,通过((zend_uintptr_t)(v))
&
GC_COLOR可以保留低点儿各的颜色信息,同时另外的号还成了0,将这结果同a进行”|”操作,就能够用a的赋给v,同时保留了v的颜色信息。

 

喻了颜色信息的存储方,那么就是活该十分容易了解什么设置与获取颜色信息,这里就是无多介绍了。

 

4.节点缓冲区

  GC会将采访及之节点存放到一个缓冲区中,缓冲区满之时节就是起展开垃圾分析算法。这个缓冲区实际上在一个大局的结构面临:

 

 

[cpp] view
plain copy

  1. typedef struct _zend_gc_globals {  

  2.     zend_bool         gc_enabled;  

  3.     zend_bool         gc_active;  

  4.   

  5.     gc_root_buffer   *buf;              /* preallocated arrays of buffers   */  

  6.     gc_root_buffer    roots;            /* list of possible roots of cycles */  

  7.     gc_root_buffer   *unused;           /* list of unused buffers           */  

  8.     gc_root_buffer   *first_unused;     /* pointer to first unused buffer   */  

  9.     gc_root_buffer   *last_unused;      /* pointer to last unused buffer    */  

  10.   

  11.     zval_gc_info     *zval_to_free;     /* temporaryt list of zvals to free */  

  12.     zval_gc_info     *free_list;  

  13.     zval_gc_info     *next_to_free;  

  14.   

  15.     zend_uint gc_runs;  

  16.     zend_uint collected;  

  17.   

  18. #if GC_BENCH  

  19.     zend_uint root_buf_length;  

  20.     zend_uint root_buf_peak;  

  21.     zend_uint zval_possible_root;  

  22.     zend_uint zobj_possible_root;  

  23.     zend_uint zval_buffered;  

  24.     zend_uint zobj_buffered;  

  25.     zend_uint zval_remove_from_buffer;  

  26.     zend_uint zobj_remove_from_buffer;  

  27.     zend_uint zval_marked_grey;  

  28.     zend_uint zobj_marked_grey;  

  29. #endif  

  30.   

  31. } zend_gc_globals;  

 

 

 

 

 

用宏GC_G(v)可拜结构中之v字段。 

粗略的牵线这结构中几乎独重要之字段的含义:

zend_bool  gc_enabled:

    是否打开GC

zend_bool  gc_active:

    GC是否正在拓展垃圾分析

gc_root_buffer   *buf:

    
节点缓冲区指针,在GC初始化的上,会分配10,000独gc_root_buffer结构的空中,buf为第1只节点的地址

gc_root_buffer    roots;

     
GC每次开头废品分析算法的上,都是于者节点开始开展(注意勿是直以缓冲区中按照顺序来分析节点,缓冲区值是存放在节点信息内容,roots是分析的节点入口,是一个双双链表的入口)

   

别节点和废品分析过程遭到的部分临时数据有关,这里少不介绍。

 

 

 5.GC的初始化

 

[cpp] view
plain copy

  1. ZEND_API void gc_init(TSRMLS_D)  

  2. {  

  3.     if (GC_G(buf) == NULL && GC_G(gc_enabled)) {  

  4.         GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);  

  5.         GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];  

  6.         gc_reset(TSRMLS_C);  

  7.     }  

  8. }  

 

 

  首先以初始化之前会起一个全局变量

  extern ZEND_API zend_gc_globals gc_globals;

   在尽GC运行期间还指之全局变量结构。

 

 
初始化是调用的gc_init函数,如果缓冲区指针字段为空并且GC开启,那么尽管分配缓冲区,然后调用gc_reset初始化全局结构gc_globals中之相干字段。

 

6.节点放入缓冲区的时机

 
那么现在便是一个于重大的一样步了,GC何时呢zval设置节点信息,并拿节点信息设置放入缓冲区等待分析处理。从前面介绍的GC算法的原理中,准则3:“如果一个zval的refcount减少下大于0,那么这zval还无可知给放飞,此zval可能成一个垃圾堆”。我们大约可以了解当一个zval的refcount减少的时节,GC有或啊zval分配节点并放入缓冲区。那么在什么动静下zval的refcount会打折扣。
在我们调用unset的上,会打当前记的哈希表中去变量名对应之项,并对该项调用一个析构函数,所以这refcount减少的操作发生在斯析构函数吃。通过建变量符号哈希表的代码段可以了解这个析构函数是啊。这个析构函数最终的贯彻以Zend/zend_execute_API.c中:

 

[cpp] view
plain copy

  1. ZEND_API void _zval_ptr_dtor(zval **zval_ptr ZEND_FILE_LINE_DC) /* {{{ */  

  2. {  

  3. #if DEBUG_ZEND>=2  

  4.     printf(“Reducing refcount for %x (%x): %d->%d/n”, *zval_ptr, zval_ptr, Z_REFCOUNT_PP(zval_ptr), Z_REFCOUNT_PP(zval_ptr) – 1);  

  5. #endif  

  6.     Z_DELREF_PP(zval_ptr);  

  7.     if (Z_REFCOUNT_PP(zval_ptr) == 0) {  

  8.         TSRMLS_FETCH();  

  9.   

  10.         if (*zval_ptr != &EG(uninitialized_zval)) {  

  11.             GC_REMOVE_ZVAL_FROM_BUFFER(*zval_ptr);  

  12.             zval_dtor(*zval_ptr);  

  13.             efree_rel(*zval_ptr);  

  14.         }  

  15.     } else {  

  16.         TSRMLS_FETCH();  

  17.   

  18.         if (Z_REFCOUNT_PP(zval_ptr) == 1) {  

  19.             Z_UNSET_ISREF_PP(zval_ptr);  

  20.         }  

  21.   

  22.         GC_ZVAL_CHECK_POSSIBLE_ROOT(*zval_ptr);  

  23.     }  

  24. }  

 

 

 这个函数中:

Z_DELREF_PP(zval_ptr) :对zval的refcount减1,减1之后

1.一旦zval的refcount等于0,根据前的准则2,这个变量的空间可以一直给假释掉,在纵前要留意,有或是变量在前面曾于放入了节点缓冲区,所以需要调用GC_REMOVE_ZVAL_FROM_BUFFER(*zval_ptr)从节点缓冲区中剔除相关节点信息,然后调用zval_dtor和efree_rel释放掉变量zval中变量占用的半空中和zval结构自身之空间。

2.要是zval的refcount等给1,根据前的轨道3,这个变量有或会见成一个垃圾堆,于是调用GC_ZVAL_CHECK_POSSIBLE_ROOT(*zval_ptr)为夫设置节点信息并放入缓冲区

 

之所以,最终是透过GC_ZVAL_CHECK_POSSIBLE_ROOT宏来产生节点并放入缓冲等待处理,相关的宏和函数代码为:

 

 

[cpp] view
plain copy

  1. #define GC_ZVAL_CHECK_POSSIBLE_ROOT(z) /  

  2.     gc_zval_check_possible_root((z) TSRMLS_CC)  

  3.   

  4. static zend_always_inline void gc_zval_check_possible_root(zval *z TSRMLS_DC)  

  5. {  

  6.     if (z->type == IS_ARRAY || z->type == IS_OBJECT) {  

  7.         gc_zval_possible_root(z TSRMLS_CC);  

  8.     }  

  9. }  

  10.   

  11. ZEND_API void gc_zval_possible_root(zval *zv TSRMLS_DC)  

  12. {  

  13.     if (UNEXPECTED(GC_G(free_list) != NULL &&  

  14.                    GC_ZVAL_ADDRESS(zv) != NULL &&  

  15.                    GC_ZVAL_GET_COLOR(zv) == GC_BLACK) &&  

  16.                    (GC_ZVAL_ADDRESS(zv) < GC_G(buf) ||  

  17.                     GC_ZVAL_ADDRESS(zv) >= GC_G(last_unused))) {  

  18.         /* The given zval is a garbage that is going to be deleted by 

  19.          * currently running GC */  

  20.         return;  

  21.     }  

  22.   

  23.     if (zv->type == IS_OBJECT) {  

  24.         GC_ZOBJ_CHECK_POSSIBLE_ROOT(zv);  

  25.         return;  

  26.     }  

  27.   

  28.     GC_BENCH_INC(zval_possible_root);  

  29.   

  30.     if (GC_ZVAL_GET_COLOR(zv) != GC_PURPLE) {  

  31.         GC_ZVAL_SET_PURPLE(zv);  

  32.   

  33.         if (!GC_ZVAL_ADDRESS(zv)) {  

  34.             gc_root_buffer *newRoot = GC_G(unused);  

  35.   

  36.             if (newRoot) {  

  37.                 GC_G(unused) = newRoot->prev;  

  38.             } else if (GC_G(first_unused) != GC_G(last_unused)) {  

  39.                 newRoot = GC_G(first_unused);  

  40.                 GC_G(first_unused)++;  

  41.             } else {  

  42.                 if (!GC_G(gc_enabled)) {  

  43.                     GC_ZVAL_SET_BLACK(zv);  

  44.                     return;  

  45.                 }  

  46.                 zv->refcount__gc++;  

  47.                 gc_collect_cycles(TSRMLS_C);  

  48.                 zv->refcount__gc–;  

  49.                 newRoot = GC_G(unused);  

  50.                 if (!newRoot) {  

  51.                     return;  

  52.                 }  

  53.                 GC_ZVAL_SET_PURPLE(zv);  

  54.                 GC_G(unused) = newRoot->prev;  

  55.             }  

  56.   

  57.             newRoot->next = GC_G(roots).next;  

  58.             newRoot->prev = &GC_G(roots);  

  59.             GC_G(roots).next->prev = newRoot;  

  60.             GC_G(roots).next = newRoot;  

  61.   

  62.             GC_ZVAL_SET_ADDRESS(zv, newRoot);  

  63.   

  64.             newRoot->handle = 0;  

  65.             newRoot->u.pz = zv;  

  66.   

  67.             GC_BENCH_INC(zval_buffered);  

  68.             GC_BENCH_INC(root_buf_length);  

  69.             GC_BENCH_PEAK(root_buf_peak, root_buf_length);  

  70.         }  

  71.     }  

  72. }  

 

 

内联函数gc_zval_check_possible_root会预先判断zval的型,如果是数组或则对象类型才出或让zval分配节点信息并放入缓冲区。只有及时片栽类型才可能来环形引用。虽然GC直接处理目标是数组和对象类型,但是于这些数组和对象中富含的任何项目变量都在GC的任务范围以内,这个内联函数最终掉用的是gc_zval_possible_root函数,下面要分析是函数中之基本点流程:

1:

 if (UNEXPECTED(GC_G(free_list) != NULL &&
                GC_ZVAL_ADDRESS(zv) != NULL &&
             GC_ZVAL_GET_COLOR(zv) == GC_BLACK) &&
             (GC_ZVAL_ADDRESS(zv) < GC_G(buf) ||
              GC_ZVAL_ADDRESS(zv) >= GC_G(last_unused))) {
  /* The given zval is a garbage that is going to be deleted by
   * currently running GC */
  return;
 }

先是检查zval节点信息是否已放入到节点缓冲区,如果都放入到节点缓冲区,则一直回到,这样保证节点缓冲区中之每个zval节点只出现同样不行。

 

2:

 if (zv->type == IS_OBJECT) {
  GC_ZOBJ_CHECK_POSSIBLE_ROOT(zv);
  return;
 }

假使zval是目标类型,则运动对象类型相关的流程,本文仅因为数组类型为例讲解,所以是流程不阐述,读者可举一反三。

 

3:

 if (GC_ZVAL_GET_COLOR(zv) != GC_PURPLE) {
  GC_ZVAL_SET_PURPLE(zv);

  …

 }

而zval没有叫记为紫色,就将该标志为紫色,表示zval被放入到节点缓冲,否则不举行后面的操作。

 

4:

如若zval的节点信息指针也空,则用吗zval分配一个gc_root_buffer节点信息。这以后会起一对判断机制,如果发现节点缓冲区已经满了征需要启动垃圾分析流程了,垃圾分析流程在函数gc_collect_cycles(TSRMLS_C); 
如果缓冲区没有满,则免见面跻身垃圾分析流程,为zval分配的节点信息会被加入到GC_G(roots)为进口的双链表中。

 

从今这函数我们发现了排泄物分析算法是当发现缓冲区满之早晚即便这触发,垃圾分析和代码执行流是同步过程,也就算是只有垃圾分析了之后,代码才会继续执行。所以当咱们的PHP代码中,如果某个unset正好要GC的节点缓冲区满,触发了排泄物分析流程,那么是unset耗费的时光将于一般的unset多博。

 

gc_collect_cycles函数是确实的杂质分析流程,这个函数定义为:

 

[cpp] view
plain copy

  1. ZEND_API int gc_collect_cycles(TSRMLS_D)  

  2. {  

  3.     int count = 0;  

  4.   

  5.     if (GC_G(roots).next != &GC_G(roots)) {  

  6.         zval_gc_info *p, *q, *orig_free_list, *orig_next_to_free;  

  7.   

  8.         if (GC_G(gc_active)) {  

  9.             return 0;  

  10.         }  

  11.         GC_G(gc_runs)++;  

  12.         GC_G(zval_to_free) = FREE_LIST_END;  

  13.         GC_G(gc_active) = 1;  

  14.         gc_mark_roots(TSRMLS_C);  

  15.         gc_scan_roots(TSRMLS_C);  

  16.         gc_collect_roots(TSRMLS_C);  

  17.   

  18.         orig_free_list = GC_G(free_list);  

  19.         orig_next_to_free = GC_G(next_to_free);  

  20.         p = GC_G(free_list) = GC_G(zval_to_free);  

  21.         GC_G(zval_to_free) = NULL;  

  22.         GC_G(gc_active) = 0;  

  23.   

  24.         /* First call destructors */  

  25.         while (p != FREE_LIST_END) {  

  26.             if (Z_TYPE(p->z) == IS_OBJECT) {  

  27.                 if (EG(objects_store).object_buckets &&  

  28.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].valid &&  

  29.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount <= 0 &&  

  30.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.dtor &&  

  31.                     !EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].destructor_called) {  

  32.   

  33.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].destructor_called = 1;  

  34.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount++;  

  35.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.dtor(EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.object, Z_OBJ_HANDLE(p->z) TSRMLS_CC);  

  36.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount–;  

  37.                 }  

  38.             }  

  39.             count++;  

  40.             p = p->u.next;  

  41.         }  

  42.   

  43.         /* Destroy zvals */  

  44.         p = GC_G(free_list);  

  45.         while (p != FREE_LIST_END) {  

  46.             GC_G(next_to_free) = p->u.next;  

  47.             if (Z_TYPE(p->z) == IS_OBJECT) {  

  48.                 if (EG(objects_store).object_buckets &&  

  49.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].valid &&  

  50.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount <= 0) {  

  51.                     EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount = 1;  

  52.                     Z_TYPE(p->z) = IS_NULL;  

  53.                     zend_objects_store_del_ref_by_handle_ex(Z_OBJ_HANDLE(p->z), Z_OBJ_HT(p->z) TSRMLS_CC);  

  54.                 }  

  55.             } else if (Z_TYPE(p->z) == IS_ARRAY) {  

  56.                 Z_TYPE(p->z) = IS_NULL;  

  57.                 zend_hash_destroy(Z_ARRVAL(p->z));  

  58.                 FREE_HASHTABLE(Z_ARRVAL(p->z));  

  59.             } else {  

  60.                 zval_dtor(&p->z);  

  61.                 Z_TYPE(p->z) = IS_NULL;  

  62.             }  

  63.             p = GC_G(next_to_free);  

  64.         }  

  65.   

  66.         /* Free zvals */  

  67.         p = GC_G(free_list);  

  68.         while (p != FREE_LIST_END) {  

  69.             q = p->u.next;  

  70.             FREE_ZVAL_EX(&p->z);  

  71.             p = q;  

  72.         }  

  73.         GC_G(collected) += count;  

  74.         GC_G(free_list) = orig_free_list;  

  75.         GC_G(next_to_free) = orig_next_to_free;  

  76.     }  

  77.   

  78.     return count;  

  79. }  

 

这边就简单的介绍其中重点之流程:

1.gc_mark_roots()

  
这个函数对节点信息的链表进行相同涂鸦深度优先遍历,将中间的zval的refcount减1,为了避免对同一个zval重复减操作,在操作之后以zval标记成灰色。(对节点自身的zval可以再次减操作,这个是此算法的基础)

 

2.gc_scan_roots()

 
这个函数对节点信息的链表再次开展深度优先遍历,如果发现zval的refcount大于等于1,则针对该zval和其含有的zval的refcount加1操作,这个是对匪垃圾的一个音讯过来,然后将这些zval颜色属性去丢(设置成black)。如果发现zval的refcount等于0,则就记成白色,这些是背后将清理掉的垃圾堆。

 

3.gc_collect_roots()

  
遍历节点信息链表,将前一个手续中标记为白色的节点信息放GC_G(zval_to_free)为进口的链表中,这个链表用来存放将要释放的污物。
然后放掉满的节点信息,缓冲区叫清空。分析了晚用更收集节点信息。

 

4.放出步骤3中收集至垃圾堆数据。

相关文章