PHP垃圾回收机制,在网上能查到的有早期的,如PHP5.3的垃圾回收机制,也有新的比如PHP7以后的垃圾回收机制。
我觉得有必要先了解一下旧的PHP5.3的垃圾回收机制,原因是简单一些,主要理解引用计数和写时复制的概念。
同时也看看早期的存在什么缺陷,再去了解PHP7的可能会更容易一些,因为这是在原来的垃圾回收机制基础上做了修改。
一.PHP5.3的垃圾回收机制
我们知道PHP定义变量的时候是不用区分各种数据类型的,这是因为PHP替我们做了记录,每当我们定义一个变量,内存中就会将变量名存入符号表,而将变量的值和类型及其一些信息存入一个叫zval的结构体中(PHP是用C写成的),举个下面的例子:
<?php
$a=10;
?>
而这个结构体zval其实长成这个样子:
truct _zval_struct {
union {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
zend_ast *ast;
} value;
zend_uint refcount__gc;
zend_uchar type;
zend_uchar is_ref__gc;
};
第一项:一个叫做union的联合体,里面存储的是变量的值(value)。
第二项:refcount_gc就是一个整数,用来记录多少个变量指向这个结构,稍后会详细介绍。(标红很重要)
第三项:type记录的是这个数据的类型,由此去解读第一项value中的内容。
第四项:is_ref_gc记录的是这个变量有没有使用过引用,大白话就是又没$b=&$a;(标红很重要)
下面我们来看看使用各种变量的时候内存中发生了什么:
情况1
<?php
$a=1;
$b=$a;
?>
我们知道$a和$b中存的是同样的内容,如果再开辟一块内存空间存储同样的内容是很浪费的,PHP早已经想到了这一点,所以
$b=$a的时候并没有给$b开辟一块新的内存空间,只是在符号表中增加了$b,让它指向和$a同样的结构体。
刚刚定义$a的时候refcount_gc=1,因为只要$a指向它,同理执行完$b=$a之后zval中的refcount_gc=2 ,这个有什么用处呢?
接着看:
情况2
<?php
$a =1;
$b = $a;
$b += 5;
?>
从逻辑上可以看到,我们修改了$b的值,而$a的值是不应该改变的。
所以不能用同一个结构体来存储$a、$b的值了,这时候PHP会在内存中开辟一块新的空间存储$b的值,这样就可以修改$b的值了:
问题来了,内核怎么知道$a和$b要分家,不应该用同一个zval呢?
答案并不复杂,内核首先查看refcount__gc属性,如果它大于1则为这个变化的变量从原zval结构中复制出一份新的专属与$b的zval来,并改变其值。
所以这个时候$a的zval中refcount__gc=1,$b的zval中refcount__gc=1;
情况3
<?php
$a =1;
$b =&$a;
$b += 5;
?>
从逻辑上我们知道 $b是$a的引用,所以改变$b的时候$a也是要改变的,所以无需开辟新的内存,不用分家。
但是内核是怎么知道不用分家的呢?
答案就是通过is_ref_gc来判断。执行$b=&$a的时候,is_ref_gc=1。
简单的讲,它是通过zval的is_ref__gc成员来获取这些信息的。
这个成员只有两个值,就像开关的开与关一样。它的这两个状态代表着它是否是一个用户在PHP语言中定义的引用。
在第一条语句($a = 1;)执行完毕后,$a对应的zval的refcount__gc等于1,is_ref__gc等于0;
当第二条语句执行后($b = &$a;),refcount__gc属性向往常一样增长为2,而且is_ref__gc属性也同时变为了1!
最后,在执行第三条语句的时候,内核再次检查$b的zval以确定是否需要复制出一份新的zval结构来,这次不需要复制.
这一次,尽管它的refcount等于2,但是因为它的is_ref等于1,所以也不会被复制。
由情况1、情况2、情况3我们可以看到这两个字段实现了,引用计数和写时复制的功能。
情况4
<?php
$a = 1;
$b = $a;
$c = &$a;
?>
童鞋们可以思考一下,这种情况内核执行第三句该怎么处理,还要不要分家呢?
我们可以想一下,如果不分家的话$a、$b、$c指向相同的zval,那么refcount_gc=3且is_ref_gc=1;
那么我们这时候修改$b的值,内核会认为$b、$c都是引用,不会给$b开辟新的内存空间,导致$a($c)的值也改变了。
所以这种情况下,执行第三句代码肯定要分家,才能不发生上面的矛盾的矛盾。结果就是:
$a与$c共用一个zval,$b自己用一个zval。
同样,下面的这段代码同样会在内核中产生歧义,所以需要强制复制!
<?php
$a = 1;
$b = &$a;
$c = $a;
?>
但是PHP5.3这种垃圾管理方式会存在一个问题,就是存在循环引用导致的内存泄露。
二.PHP5.3循环引用导致的内存泄露
先看代码:
<?php
$a = ['one'];
$a[] = &$a;
xdebug_debug_zval('a');
?>
注意unset()函数是取消某个变量在符号表中的注册,如果:
<?php
$a=10;
$b=&$a;
unset($a);
?>
这时候$b依然存在,只是$a在符号表中被取消了。
回到第一段代码,当我们执行$a[]=&$a,的时候很自然会得到$a指向的zval中:refcount_gc=2,is_ref_gc=1;
此时 $a数组就有了两个元素,一个索引为0,值为one字符串,另一个索引为1,为$a自身的引用。
当我们执行unset($a)的时候,$a符号会被注销掉,同时refcount_gc减1 ,所以refcount_gc=1
$a已经不在符号表了,没有变量再指向此zval容器,用户已无法访问,但是由于数组的refcount变为1而不是0,导致此部分内存不能被回收从而产生了内存泄漏。
只有在PHP脚本结束的时候才能释放该部分内存,如果这样的内存泄露有很多的话,运行速度将产生极大的影响。
所以PHP7的垃圾回收机制对zval做了改动,不仅解决了环形引用导致的内存泄露问题,也做了很多优化。
我将在另一篇博客介绍PHP新的垃圾回收机制。
参考资料:
https://www.cnblogs.com/fengwei/p/3775062.html
https://blog.csdn.net/weixin_41282397/article/details/84969162
https://blog.csdn.net/xuduorui/article/details/76462123(很全的一篇博客)
2020/09/07
深入理解PHP7内核之zval
浅析 ZVAL