PHP垃圾回收机制(一)

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值