diffnest / myblog

0 stars 0 forks source link

PHP垃圾回收以及内存泄漏 #25

Open diffnest opened 6 years ago

diffnest commented 6 years ago

PHP垃圾回收以及内存泄漏

判断是否为垃圾的一个重要标准是有没有变量名指向变量容器zval,垃圾回收说到底是对变量及其所关联内存对象的操作,即当变量的引用数等于0时,就会被销毁;

  1. 首先看下存储结构体(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;
    zend_ast *ast;
} zvalue_value;

struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */ 即是上述所说的zvalue_value,也就是存储的数据
    zend_uint refcount__gc; //即是变量的引用数,变量引用计算器。
    zend_uchar type;    /* active type */ 变量的类型,为了保存当前所存储的数据,是哪一个类型的数据
    zend_uchar is_ref__gc; //变量是否被引用
};
  1. 生成垃圾:

<?php
$a = array('one');
xdebug_debug_zval('a');

$a[] = & $a;
xdebug_debug_zval('a');

unset($a);
xdebug_debug_zval('a');

结果如下:

a: (refcount=2, is_ref=0)=array (
    0 => (refcount=1, is_ref=0)='one'
)
a: (refcount=2, is_ref=1)=array (
    0 => (refcount=2, is_ref=0)='one', 
    1 => (refcount=2, is_ref=1)=...
)
a: no such symbol

如图所示: unset前:此时$a的内部构造可以形象的用下图表示: default

unset后:此时的情况可以用下图表示:

default

当执行unset($a);此时$a已经不在符号表中了,用户无法再访问此变量,但是$a之前指向的zval的refcount变为1而不是0,因此不能被回收,这样产生了内存泄露; unset只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数-1;

  1. GC回收实现:

    首先我们有几个基本的准则: 1:如果一个zval的refcount增加,那么此zval还在使用,不属于垃圾

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

3:如果一个zval的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾

只有在准则3下,GC才会把zval收集起来,然后通过新的算法来判断此zval是否为垃圾。 default

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,那么可以判断这个数组是一个垃圾。

参考链接:1. http://blog.csdn.net/phpkernel/article/details/5734743

  1. http://blog.csdn.net/u011250882/article/details/49564043