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