Php引用计时器和垃圾回收机制
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
php引用计数器和垃圾回收机制谈到引用计数器和垃圾回收机制,必须得从php变量说起。
总所周知,php 是一种弱类型,但具体表现在哪里,程序里面又是怎么表现的呢?php里面又是怎样实现引用计数器的,程序如何区分变量引用和复制?php是如何对已用完的变量进行回收,不同的php版本的不同的垃圾回收机制又是如何实现的?
1.引用计数器
讲到引用计数器,不得不先说一下变量的c语言实现。
如下,几个变量的结构体和联合体:
zvalue_value联合体:
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
H as hTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;
zval的结构:
struct _zval_struct {
/* Variable information */
zvalue_value value;
/* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
zval可以看成一个容器,zvalue_value是该容器存储变量值的联合体,refcount__gc 是引用计数,记录引用数,is_ref__gc是标志这个容器是否真正的引用,type表示这个变量的类型。
根据这个表格可以发现两个有意思的地方:首先是PHP的数组其实就是一个HashTable,这就解释了为什么PHP能够支持关联数组了;其次,Resource就是一个long值,它里面存放的通常是个指针、一个内部数组的index或者其它什么只有创建者自己才知道的东西,可以将其视作一个handle。
(1)写复制(Copy on Write)
<?php
$var = "laruence";
$var_dup = $var;
$var = 1;
?>
PHP在修改一个变量以前,会首先查看这个变量的refcount,如果refcount大于1,PHP就会执行一个分离的例程,对于上面的代码,当执行到第三行的时候,PHP发现$var 指向的zval的refcount大于1,那么PHP就会复制一个新的zval出来,将原zval的refcount 减1,并修改symbol_table,使得$var和$var_dup分离(Separation)。
这个机制就是所谓的copy on write(写时复制)。
题外话:写时复制技术的一个比较有名的应用是在unix类操作系统内核中,当一个进程调用fork 函数生成一个子进程的时候,父子进程拥有相同的地址空间内容,在老版本的系统中,子进程是在fork 的时候就将父进程的地址空间中的内容都拷贝一份,对于规模较大的程序这个过程可能会有着很大的开销,更崩溃的是,很多进程在fork之后,直接在子进程中调用exec执行另外一个程序,这样原来花了大量时间从父进程复制的地址空间都还没来得及碰一下就被新的进程地址空间代替,这显然是对资源的极大浪费,所以在后来的系统中,就使用了写时复制技术,fork之后,子进程的地址空间还是简单的指向父进程的地址空间,只有当子进程需要写地址空间中的内容的时候,才会单独分离一份(一般以内存页为单位)给子进程,这样就算子进程马上调用exec函数也没关系,因为根本就不需要从父进程的地址空间中拷贝内容,这样节约了内存同时又提高了速度。
(2)写改变(change on writ )
开始在zval里面我们看到一个字段is_ref__gc,到底如何是怎样产生作用的呢?
现在我们知道,当使用变量复制的时候,PHP内部并不是真正的复制,而是采用指向相同的结构来尽量节约开销。
那么,对于PHP中的引用,那又是如何实现呢?
<?php
$var = "laruence";
$var_ref = &$var;
$var_ref = 1;
?>
这段代码结束以后,$var也会被间接的修改为1,这个过程称作(change on write:写时改变)。
那么ZE是怎么知道,这次的复制是不需要Separation的呢?
这个时候就要用到zval中的is_ref字段了:
对于上面的代码,当第二行执行以后,$var所代表的zval的refcount变为2,并且同时置is_ref为1。
当使用引用时,php会把is_ref__gc即程序会如下判断该引用是否真实引用,P HP 先检查var_ref代表的zval的is_ref__gc字段,如果为1,则不分离,大体逻辑示意如下:
<?php
if((*val)->is_ref || (*val)->refcount<2){
//不执行Separation
... ; //process
}
?>
(3)两种方式到底怎样使用,啥时候使用?
<?php
$var = "laruence";
$var_dup = $var;
$var_ref = &$var;
?>
对于上面的代码,存在一对copy on write的变量$var和$var_dup, 又有一对change on write机制的变量对$var和$var_ref,这个情况又是如何运作的呢?
当第二行执行的时候,和前面讲过的一样,$var_dup 和$var 指向相同的zval,refcount为2。
当执行第三行的时候,PHP发现要操作的zval的refcount大于1,则,PHP会执行Separation, 将$var_dup分离出去,并将$var和$var_ref做change on write关联。
也就是,refcount=2, is_ref=1。
(4)数值巨大变量处理
当然如果我们在php中进行参数传递的时候,是否有必要对传递内容巨大的数组心存警惕,担心内存的大量流失?
<?php
function countChina($persons)
{
$count=0;
foreach($persons as $person)
{
if($person['nation'] == 'china')
$count++;
}
return $count;
}
?>
例如上面这段统计一群人中有多少个中国国籍的时候,如果有一个10W的人的数组要传进去,你是否会担心被复制后传到函数里,导致内存占用瞬间翻倍?也许你会在参数那里加一个&表示引用?
php的设计者在这里有一个很巧妙的设计,引入了一个copy on write的概念,在上面的函数中,如果你不去修改$perons对象的内容,并不会有复制行为发生,内存也不会double.
但是如果我想在函数里面修改数据,便于函数后面的处理,这个时候内存会double吗?
如下:
<?php
function countChina($persons)
{
$count=0;
foreach($persons as $person)
{
if($person['nation'] == 'china')
{
$person['score'] = 10;
$count++;
}
}
//利用score做一些处理...
return $count;
}
在上面的代码中,我修改了数组的成员内容,如果你不在参数里加&的话,内存消耗会逐步增加,在返回以前,内存的占用会double,为撒呢?因为为了满足你的write需要,php进行了copy,这个copy不是一次完成的,在改变第一个$persons成员的时候,会将$persons数组和其成员的地址复制过来,并不会把所有成员内容都复制过来,随着$persons成员的一个个被write,一个个也都被copy过来,内存占用线性增加,到循环结束时,double了!如果确实要修改内容,又想避免内存消耗,又不怕影响参数变量在其他地方的使用,那就在参数里加&吧。
要释放被占用的内存变量,用unset($persons)或者$persons=null都可以迅速地释放掉,你可以使用memory_get_usage进行监控,貌似这个机制比.net体系的垃圾回收机制要清晰,简单和高效,经常出现的out of memory无疑是.net庞大体系和低效内存回收机制的必然结果。
(5)unset的作用
unset()并非一个函数,而是一种语言结构,这个可以通过查看编译生成的opcode看到区别,unset对应的不是一个函数调用的opcode。
那么unset到底做了什么?在unset 对应的opcode的handler中可以看到相关内容,主要的操作时从当前符号表中删除参数中的符号,比如在全局代码中执行unset($a),那么将会在全局符号表中删除a这个符号。
全局符号表是一张哈希表,建立这张表的时候会提供一个表中的项的析构函数,当我们从符号表中删除a的时候,会对符号a指向的项(这里是zval的指针)调用这个析构函数,这个析构函数的主要功能是将a对应的zval的refcount减1,如果refcount变成了0,那么释放这个zval。
所以当我们调用unset的时候,不一定能释放变量所占的内存空间,只有当这个变量对应的zval没有别的变量指向它的时候,才会释放掉zval,否则只是对refcount进行减1操作。
2.垃圾回收机制
(1)PHP5.2中的垃圾回收算法——Reference Counting
PHP5.2中使用的内存回收算法是大名鼎鼎的Reference Counting,这个算法中文翻译叫做“引用计数”,其思想非常直观和简洁:为每个内存对象分配一个计数器,当一个内
存对象建立时计数器初始化为1(因此此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。
而PHP中内存对象就是zval,而计数器就是refcount__gc。
Reference Counting简单直观,实现方便,但却存在一个致命的缺陷,就是容易造成内存泄露。
很多朋友可能已经意识到了,如果存在循环引用,那么Reference Counting 就可能导致内存泄露。
例如下面的代码:
1.<?php
2.
3.$a = array();
4.$a[] = & $a;
5.unset($a);
6.
7.?>
这段代码首先建立了数组a,然后让a的第一个元素按引用指向a,这时a的zval的refcount就变为2,然后我们销毁变量a,此时a最初指向的zval的refcount为1,但是我们再也没有办法对其进行操作,因为其形成了一个循环自引用,如下图所示:
(2)PHP5.3中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems
首先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、可以总将内存泄露保持在一个阈值以下。
(3)Php5.2和php5.3性能比较
可以看到在可能引发累积性内存泄露的场景下,PHP5.2发生持续累积性内存泄露,而PHP5.3则总能将内存泄露控制在一个阈值以下(与根缓冲区大小有关)。
3.参考地址
(1)php4中的zval溢出漏洞:
/article/info/info.php?infoid=31838
(2)php5.2和php5.3的垃圾回收机制:
/edu/2011/03-12/4369.html
(3)php5.3的垃圾回收机制论文(Concurrent Cycle Collection in Reference Counted Systems):
/people/d/dfb/papers/Bacon01Concurrent.pdf
(4)php引用计数器:
/manual/zh/features.gc.refcounting-basics.php。