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

baiyanphp

所有视频:https://segmentfault.com/a/11...算法

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

因为这个系列的视频后面会再次细讲垃圾回收,那么咱们今天先复习一下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,很好理解:

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

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

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

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

  • 咱们看到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。]见下图:

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


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

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

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

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

  • 咱们能够看到,$a的type类型变成了0(IS_UNDEF),同时其指向的zend_reference结构体的refcount变为了1(由于$a数组中的元素仍然在指向它),咱们画图来表示一下如今的内存状况:

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