【PHP源码学习】2019-03-19 PHP引用

【PHP源码学习】2019-03-19 PHP引用

baiyan

原视频地址:http://replay.xesv5.com/ll/26...

由于这个系列的视频后面会再次细讲垃圾回收,那么我们今天先复习一下PHP中的引用,为后面做一个铺垫,后续的笔记会详细讲解垃圾回收器的相关运行原理。

PHP7中的引用

  • 引用:可以通过不同的变量名,访问同一个变量内容。
  • PHP7中的引用通过让两个变量指向同一块内存空间实现了上述特性。在进行引用赋值后,等号左右两边的变量均变成了引用类型(IS_REFERENCE)。这块公用的内存空间就是PHP7为引用类型的变量专门创建的一个结构体,叫做zend_reference。
  • 代码示例:
$a = 1;
echo $a;
$b = &$a; //$b是$a的引用
echo $a;
echo $b;
unset($b);
echo $a;
  • 我们用gdb调试以上代码:
  • 首先执行$a = 1;并且打印$a的值,$a就是一个普通的zval,其类型是IS_LONG,很好理解:

【PHP源码学习】2019-03-19 PHP引用

  • 执行关键的一步:$b = &$a,打印$a的值,观察$a的存储情况:

【PHP源码学习】2019-03-19 PHP引用

  • 观察上图,可以发现$a的type变成了10 (IS_REFERENCE)类型,并且ref字段指向了一个新的结构体,这就是zend_reference,zend_reference中存储着$a与$b共同的值1,由于$a与$b同时引用着这个结构体,故此时该结构体的refcount = 2。
  • 接下来打印$b,观察$b的存储情况:

【PHP源码学习】2019-03-19 PHP引用

  • 观察上图,发现与$b的type也是IS_REFERENCE类型,且ref字段也指向了一个zend_reference结构体,比较$a与$b指向的zend_reference,二者地址相同,说明指向了同一个zend_reference结构体。此时两个变量的存储情况如下图所示:

【PHP源码学习】2019-03-19 PHP引用

  • 接下来执行unset($b),观察$a以及zend_reference的存储情况,我们看是否符合预期:

【PHP源码学习】2019-03-19 PHP引用

  • 我们看到unset($b)之后,$a所指向的zend_reference的refcount由2变为1,说明现在只有$a引用着这个结构体,b不再引用这个结构体,其类型变成了IS_UNDEF类型,代码执行完毕。
  • 那么我们看一下zend_reference结构体的基本结构:
struct _zend_reference {
    zend_refcounted_h gc; //gc相关,存有refcount
    zval  val;   //引用类型的变量值存在这个zval中的zend_value字段中。简单类型的值直接存在这里,复杂类型的值存储对应数据结构的指针,来找到这个变量的值,和之前讲基本变量时候讲过的一样。
};
  • 这个结构体一共只有2个字段,gc字段中是zend_refcounted_gc结构体类型,其中存储了引用计数;val字段存储了引用类型变量的值(简单类型如整型、浮点型的值直接存在这里,复杂类型存对应数据结构的指针,与之前讲基本变量的时候讲过的一样)。这样相当于加了一个中间层,使得原始的zend_string或zend_array在内存中只有1份,方便管理与维护。

循环引用问题

  • 循环引用:变量自己引用自己。
  • 我们首先构造一个循环引用:
<?php
$a = ['time' => time()];
echo $a;
$a[] = &$a; //循环引用
echo $a;
unset($a);
echo $a;
  • 注意:由于开启opcache的PHP7会在数组初始化的元素全部为常量元素的时候,将其优化成不可变数组(immutable array),这里的引用计数值refcount = 2只是一个伪引用计数,所以我们使用$a = ['time' => time()],让其初始化后的refcount为正常的1。]见下图:

【PHP源码学习】2019-03-19 PHP引用

  • 利用gdb调试这段代码:
  • 执行完$a初始化并打印$a,refcount为1,type为7(IS_ARRAY)而此时的ref字段中的值是非法地址,说明此时还没有生成中间的zend_reference结构体:

【PHP源码学习】2019-03-19 PHP引用
【PHP源码学习】2019-03-19 PHP引用

  • 继续执行下一行$a[] = &$a; 观察下图中绿色方框的含义:

    - $a的zval中的ref指向zend_reference结构体
    - zend_reference结构体中的zval字段中的arr指针指向了原始的zend_array
    - zend_array中的arData指针指向了bucket类型
    - zend_array中的bucket数组元素也是一个IS_REFERENCE类型,它又指回到同一个zend_reference结构体:

【PHP源码学习】2019-03-19 PHP引用

  • 根据gdb调试情况画出内存结构图:

【PHP源码学习】2019-03-19 PHP引用

  • 由于有两个东西指向zend_reference结构体(一个是$a,一个是$a数组中的一个元素),所以refcount = 2。原始的zend_array中也有一个refcount字段,由于只有一个zend_reference指向这个zend_array,所以refcount = 1。
  • 接下来继续执行unset($a):

【PHP源码学习】2019-03-19 PHP引用

  • 我们可以看到,$a的type类型变成了0(IS_UNDEF),同时其指向的zend_reference结构体的refcount变为了1(因为$a数组中的元素仍然在指向它),我们画图来表示一下现在的内存情况:

【PHP源码学习】2019-03-19 PHP引用

  • 那么问题出现了,$a是unset掉了,但是由于原始的zend_array中的元素仍然在指向仍然在指向zend_reference结构体,所以zend_reference的refcount是1,而并非是预期的0。这样一来,这两个zend_reference与zend_array结构在unset($a)之后,仍然存在于内存之中,如果对此不作任何处理,就会造成内存泄漏。
  • 那么如何解决循环引用带来的内存泄漏问题呢?垃圾回收就要派上用场了。在PHP7中,如果检测到refcount -1 后仍 > 0的变量,会把它放入一个双向链表中,等待垃圾回收,相当于一个缓冲区的作用。待缓冲区满了之后(10000个存储单元),然后再对其进行标记和清除(以后会在代码层面具体讲垃圾回收的方法)。
  • 缓冲区的作用就是减少垃圾回收算法运行的频率,减少对正在运行的服务端代码的影响。

相关推荐