php中垃圾回收机制

php中垃圾回收机制

 

我们可能在开发中经常会听到gc,是的gc就是垃圾回收容器,全称Garbage Collection。

 

此篇文章中“垃圾”的概念:如果一个变量容器能被减少到0,说明他就已经没有被引用了,属于正常销毁,所以不属于垃圾,而垃圾是指当外部引用被全部清除后,引用计数还不为0的变量容器

引用计数基本知识

每个php变量存在一个叫"zval"的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。第一个是"is_ref",是个bool值,用来标识这个变量是否是属于引用集合(reference set)。通过这个字节,php引擎才能把普通变量和引用变量区分开来,由于php允许用户通过使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用。第二个额外字节是"refcount",用以表示指向这个zval变量容器的变量(也称符号即symbol)个

当一个变量被赋常量值时,就会生成一个zval变量容器,如下例这样:

如果安装了xdebug,可以通过调用函数 xdebug_debug_zval()显示"refcount"和"is_ref"的值。

<?php
$a = "new string";
xdebug_debug_zval('a');
?>
a: (refcount=1, is_ref=0)='new string'

 

把一个变量赋值给另一变量将增加引用次数(refcount).

 

$a = ['name'=>'lq','number'=>3];  //创建一个变量容器,变量a指向给变量容器,a的ref_count为1
$b = $a;//变量b也指向变量a指向的变量容器,a和b的ref_count为2
xdebug_debug_zval('a', 'b'); 
$b['name'] = '我的的技术成长之路';//变量b的其中一个元素发生改变,此时会复制出一个新的变量容器,变量b重新指向新的变量容器,a和b的ref_count变成1
xdebug_debug_zval('a', 'b'); 

以上将会输出:

a: (refcount=2, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='lq', 'number' => (refcount=1, is_ref=0)=3) 
b: (refcount=2, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='lq', 'number' => (refcount=1, is_ref=0)=3)
a: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='lq', 'number' => (refcount=1, is_ref=0)=3)
b: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='lq', 'number' => (refcount=1, is_ref=0)=3)

所以,当变量a赋值给变量b的时候,并没有立刻生成一个新的变量容器,而是将变量b指向了变量a指向的变量容器,即内存"共享";而当变量b其中一个元素发生改变时,才会真正发生变量容器复制,这就是写时复制技术

 

因为同一个变量容器被变量 a 和变量 b关联。函数执行结束,或者对变量调用了函数 unset()时,”refcount“就会减1,下面的例子就能说明:

$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );

引用计数清0

当变量容器的ref_count计数清0时,表示该变量容器就会被销毁,实现了内存回收,这也是php5.3版本之前的垃圾回收机制

 

 

以上将会输出:

a: (refcount=3, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'

// 说明:unset并非一定会释放内存,当有两个或以上的变量指向的时候,并非会释放变量占用的内存,只是refcount减1.

 

 

当我们使用引用赋值时:

$name = "一路向北";
$temp_name = &$name;
xdebug_debug_zval('name');

 

输出结果:

name:(refcount=2, is_ref=1),string '一路向北' (length=18)

 

是的引用赋值会导致zval通过is_ref来标记是否存在引用的情况。

 

数组类型:

 

$name = ['a'=>'apple', 'b'=>'big_apple'];
xdebug_debug_zval('name');

 

 

 

我们会得到:

name:
(refcount=1, is_ref=0),
array (size=2)
  'a' => (refcount=1, is_ref=0),string 'apple' (length=9)
'b' => (refcount=1, is_ref=0),string 'big_apple' (length=9)

 

这个结构应该也很好理解:对于数组来说是一个整体,对于数组中的k=>v来说也是一个独立的整体,各自维护自己的zval的refount和is_ref。

 

php的内存管理机制

//获取内存方法,加上true返回实际内存,不加则返回表现内存
var_dump(memory_get_usage());
$name = "一路向北"; var_dump(memory_get_usage()); unset($name); var_dump(memory_get_usage());

 

 

int 1593048
int 1593264
int 1593048

 

过程:定义变量->增加内存->变量清除->恢复内存

看个例子:

$name = str_repeat('1',255);     //产生由255个1组成的字符串 
$memory = memory_get_usage();    //获取当前占用内存 
unset($name); 
$memory_s = memory_get_usage();   //unset()后再查看当前占用内存 
echo $memory -$memory_s  ; 

//最后输出unset()之前占用内存减去unset()之后占用内存,
//如果是正数那么说明unset($name)已经将$name从内存中销毁(或者 说,unset()之后内存占用减少了)
//可是得到的结果是:-48,这是否可以说明unset($name)并没有起 到销毁变量$name所占用内存的作用 

 

$name = str_repeat('1',256);   //产生由256个1组成的字符串 
$memory = memory_get_usage();    //获取当前占用内存 unset($name); 
$memory_s= memory_get_usage();   //unset()后再查看当前占用内存 
echo $memory-$memory_s; 
这个例子和上面的例子几乎相同,唯一的不同是,$name由256个1组成,即比第一个例子多了一个1
得到结果是:224。这是否可以说明,unset($name)已经将$name所占用的内存销毁了 

 

$name = str_repeat('1',256);       //这和第二个例子完全相同 
$php = &$name; 
$memory  = memory_get_usage(); 
unset($name);                      //销毁$name 
$memory_s = memory_get_usage(); 
echo $php . '<br />'; 
echo $memory-$memory_s; 

/**
* 我们看到第一行有256个1,第二行是-48,按理说我们已经销毁了$name,*而$php只是引用$name的变量
* 应该是没有内容了,另外,unset($name)后内存占用却比unset()前增加了
*/

 

$name = str_repeat('1', 256);      //这和第二个例子完全相同 
$php = &$name; 
$memory   = memory_get_usage(); 
unset($name);                      //销毁$name 
unset($php);                       //销毁$php 
$memory_s= memory_get_usage(); 
echo $php . '<br />'; 
echo$memory-$memory_s; 

/**
* 我们将$name和$php都使用unset()销毁,这时再看内存占用量之差也是* * 224,说明这样也可以释放内存 
*/

由此得到结论是:unset()函数只能在变量值占用内存空间超过256字节时才会释放内存空间,并且如果变量存在引用赋值,那么需要指向该存储单元的所以变量都销毁才会释放内存空间

 

老版本php中如何产生内存泄漏垃圾?

产生内存泄漏主要真凶:环形引用。

一个经典的场景:

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

 debug的输出:

a:
(refcount=2, is_ref=1),
array (size=2)
  0 => (refcount=1, is_ref=0),string 'one' (length=3)
  1 => (refcount=2, is_ref=1),
          &array<  // 递归引用自身

这样 $a数组就有了两个元素,一个索引0,值为one字符串,另一个索引为1,为$a自身的引用。

借用一下官方的图:

 

 

 接下来我们删掉$a:

$a = ['one'];
$a[] = &$a;
unset($a);

得到: 

(refcount=1, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=1, is_ref=1)=...
)

 

 

 如果在小于php5.3的版本就会出现一个问题:$a已经不在符号表了没有变量再指向此zval容器,用户已无法访问,但是由于数组的refcount变为1而不是0,导致此部分内存不能被回收从而产生了内存泄漏

 

 

5.3版本以后php是如何处理垃圾内存的?

新的垃圾回收机制

php5.3版本之后引入根缓冲机制,即php启动时默认设置指定zval数量的根缓冲区(默认是10000),当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量(默认是10000)后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题

为解决环形引用导致的垃圾,产生了新的GC算法,遵守以下几个基本准则:

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

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

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

引用php官方手册的配图:

以下解释属于个人理解,如有问题或错误请评论区留言。

A:为了避免每次变量的refcount减少的时候都调用GC的算法进行垃圾判断,算法会在满足准则3情况下的zval节点放入一个节点(root)缓冲区(root buffer)(自我理解:当php发现有存在循环引用的zval时,就会把其投入到根缓冲区),并且将这些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释放掉。

总结:

 

  1. unset函数:unset只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数-1;内存是否回收主要还是看refcount是否到0了,以及gc算法判断。
  2. = null 操作;a=null将a 指向的数据结构置空,同时将其引用计数归0。
  3. 脚本执行结束;脚本执行结束,该脚本中使用的所有内存都会被释放,不论是否有引用环。
  4. 以php的引用计数机制为基础(php5.3以前只有该机制),同时使用根缓冲区机制,当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题(php5.3开始引入该机制)

 

posted @ 2020-04-28 18:40  lqian  阅读(275)  评论(0编辑  收藏  举报