PHP垃圾回收机制(GC)

PHP垃圾回收机制(GC)

     前言

      垃圾回收是一个多数编程语言中都带有的内存管理机制。与非托管性语言相反:C, C++ 和 Objective C,用户需要手动收集内存,带有 GC 机制的语言:Java, javaScript 和 PHP 可以自动管理内存。

      下面是阅读 《官方手册-垃圾回收机制》后,根据我自己的理解做的笔记。方便日后查阅。

      1、引用计数的基本知识

      我们要了解GC,那么首先要了解引起垃圾回收的计数是什么。

      在php中,每个变量存在一个叫“zval”的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括另外两个字节的额外信息:is_refrefcount

      当一个变量被赋常量值时,就会生成一个zval变量容器。

      1)is_ref

      is_ref是个bool值,用来标识这个变量是否是属于引用集合。通过这个字节,php引擎才能把普通变量和引用变量区分开来。由于php允许用户通过"&"来使用自定义的引用,所以zval中还有一个内部引用计数机制,来进行优化内存。

      2)refcount

      refcount用以表示指向这个zval变量容器的变量(也称符号即symbol)的个数。所有符号存在一个符号表当中,每个符号都有作用域。

     3)简单来讲:

      refcount就是多少个变量是一样的用了相同的值,那么refcount就是这个值。  

      is_ref就是当有变量用了&的形式进行赋值,那么refcount的值就会增加1

      2、举例

      接下来的案例是以php5.6版本下运行的

      1)Example #1 

      当一个变量被赋常量值时,就会生成一个zval变量容器,如下例这样:

1 Example #1 
2 <?php
3 $a = "new string";
4 xdebug_debug_zval( 'a' );  //显示zval的信息
5 
6 输出:
7 a: (refcount=1, is_ref=0)='new string' 
8 ?>

      2)Example #2

      增加一个zval的引用计数

1 <?php
2 $a = "new string";
3 $b = $a;
4 xdebug_debug_zval( 'a' );
5 
6 输出:
7 a: (refcount=2, is_ref=0)='new string'

      分析:这时,引用次数是2,因为同一个变量容器被变量 a 和变量 b关联.当没必要时,php不会去复制已生成的变量容器。

      3)Example #3

      减少引用计数

 1 <?php
 2 $a = "new string";
 3 $c = $b = $a;
 4 xdebug_debug_zval( 'a' );
 5 unset( $b, $c );
 6 xdebug_debug_zval( 'a' );
 7 
 8 输出:
 9 a: (refcount=3, is_ref=0)='new string'
10 a: (refcount=1, is_ref=0)='new string'

      分析:当任何关联到某个变量容器的变量离开它的作用域(比如:函数执行结束),或者对变量调用了函数 unset()时,”refcount“就会减1。变量容器在”refcount“变成0时就被销毁。如果我们在上面的例子中继续执行 unset($a),包含类型和值的这个变量容器就会从内存中删除。

      4)Example #4

      当考虑像 array和object这样的复合类型时,事情就稍微有点复杂. 与 标量(scalar)类型的值不同,array和 object类型的变量把它们的成员或属性存在自己的符号表中。这意味着下面的例子将生成三个zval变量容器

1 <?php
2 $a = array( 'meaning' => 'life', 'number' => 42 );
3 xdebug_debug_zval( 'a' );
4 
5 输出:
6 a: (refcount=1, is_ref=0)=array (
7    'meaning' => (refcount=1, is_ref=0)='life',
8    'number' => (refcount=1, is_ref=0)=42
9 )

       图示:

       

       分析:这三个zval变量容器是: a,meaning和 number。增加和减少”refcount”的规则和上面提到的一样. 下面, 我们在数组中再添加一个元素,并且把它的值设为数组中已存在元素的值

       5)Example #5

       添加一个已经存在的元素到数组中 

 1 <?php
 2 $a = array( 'meaning' => 'life', 'number' => 42 );
 3 $a['life'] = $a['meaning'];
 4 xdebug_debug_zval( 'a' );
 5 
 6 输出:
 7 a: (refcount=1, is_ref=0)=array (
 8    'meaning' => (refcount=2, is_ref=0)='life',
 9    'number' => (refcount=1, is_ref=0)=42,
10    'life' => (refcount=2, is_ref=0)='life'
11 )

      图示:

     

       分析:从以上的xdebug输出信息,我们看到原有的数组元素和新添加的数组元素关联到同一个"refcount"2的zval变量容器. 尽管 Xdebug的输出显示两个值为'life'的 zval 变量容器,其实是同一个。

       6)Example #6

 

       从数组中删除一个元素

 1 <?php
 2 $a = array( 'meaning' => 'life', 'number' => 42 );
 3 $a['life'] = $a['meaning'];
 4 unset( $a['meaning'], $a['number'] );
 5 xdebug_debug_zval( 'a' );
 6 
 7 输出:
 8 a: (refcount=1, is_ref=0)=array (
 9    'life' => (refcount=1, is_ref=0)='life'
10 )

         分析:删除数组中的一个元素,就是类似于从作用域中删除一个变量. 删除后,数组中的这个元素所在的容器的“refcount”值减少,同样,当“refcount”为0时,这个变量容器就从内存中被删除

         7)Example #7

         把数组作为一个元素添加到自己

         现在,当我们添加一个数组本身作为这个数组的元素时,事情就变得有趣,下个例子将说明这个。例中我们加入了引用操作符,否则php将生成一个复制。

 1 <?php
 2 $a = array( 'one' );
 3 $a[] =& $a;
 4 xdebug_debug_zval( 'a' );
 5 
 6 输出:
 7 a: (refcount=2, is_ref=1)=array (
 8    0 => (refcount=1, is_ref=0)='one',
 9    1 => (refcount=2, is_ref=1)=...
10 )
11 ?>

     

      分析:这样$a数组就有两个元素,一个索引为0,值为字符one,另外一个索引为1,为$a自身的引用。“…”说明发生了递归操作,意味着"..."指向原始数组。

      8)Example #8

1 <?php
2 $a = array( 'one' );
3 $a[] =& $a;
4 unset($a);
5 xdebug_debug_zval( 'a' );
6 
7 输出:
8 a: no such symbol
9 ?>

      图示:

     

      分析: 那么问题也就产生了,$a已经不在符号表中了,用户无法再访问此变量,但是$a之前指向的zval的refcount变为1而不是0,因此不能被回收。

     清理变量容器的问题

      尽管不再有某个作用域中的任何符号指向这个结构(就是变量容器),由于数组元素“1”仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。

     GC算法

      1、为解决上面这种垃圾,产生了新的GC

      在PHP5.3版本中,使用了专门GC机制清理垃圾,在之前的版本中是没有专门的GC,php无法处理循环的引用内存泄露。那么垃圾产生的时候,没有办法清理,内存就白白浪费掉了。

      但是自5.3之后php使用引用计数系统中同步周期回收的同步算法,仅处理这个内存泄露问题。

      基本准则:

      1)如果一个zval的refcount增加,那么表明该变量的zval还在使用,不属于垃圾

      2)如果一个zval的refcount减少到0,那么zval可以被释放掉,可以清除,不属于垃圾

      3)如果在经过模拟删除后一个zval的refcount减1,如果该zval的引用次数为是大于0,那么此zval不能被释放,可能是一个垃圾

      4、垃圾回收周期(Collecting Cycles)

      三色机制:

      

 

       回收的过程结合gc的结构一起来分析:

 1 #gc 的结构 zend_refcounted_h 具体如下:
 2 typedef struct _zend_refcounted_h {
 3     uint32_t         refcount; // 记录 zend_value 的引用数
 4     union {
 5         struct {
 6             zend_uchar    type,  // zend_value的类型, 与zval.u1.type一致
 7             zend_uchar    flags, 
 8             uint16_t      gc_info // GC信息,记录在 gc 池中的位置和颜色,垃圾回收的过程会用到
 9         } v;
10         uint32_t type_info;
11     } u;
12 } zend_refcounted_h;

      1)步骤A (放入根缓冲区

      为了避免不得不检查所有引用计数可能减少的垃圾周期,同步算法将所有可能根放在了根缓冲区(root buffer)中(在图中用紫色来标记,称为疑似垃圾),这样可以同时确保每个可能的垃圾根在缓冲区中只出现一次。仅当根缓冲区满了时,才对缓冲区中所有不同的变量容器执行垃圾回收操作,在图中体现为步骤A。

     底层实现:

     1.  如果当变量的 refcount 减小后大于 0,PHP 并不会立即对这个变量进行垃圾鉴定和回收,而是放入一个缓冲区中,等这个缓冲区满了以后 (默认是10000 个值) 再统一进行处理,加入缓冲区的是变量 zend_value 里的 gc,目前垃圾只会出现在数组和对象两种类型中,数组的情况上面已经介绍了,对象的情况则是成员属性引用对象本身导致的,其它类型不会出现这种变量中的成员引用变量自身的情况,所以垃圾回收只会处理这两种类型的变量。

     2. 一个变量只能加入一次缓冲区,为了防止重复加入,变量加入后会把 zend_refcounted_h.gc_info 置为 GC_PURPLE,即标为紫色,后续不会重复插入。

     2)步骤B(模拟删除

     在步骤B中,模拟删除每个紫色的变量。模拟删除时可能将不是紫色的不同变量引用数减1,如果某个普通变量引用计数变成0时,就对这个普通变量在做一次模拟删除。每个变量只能被模拟删除一次,模拟删除后标记为灰色

     底层实现:

     从缓冲区链表的 roots 开始遍历,把当前 value 标识为灰色 (zend_refcounted_h.gc_info 置为 GC_GREY),然后对当前 value 的成员进行深度优先遍历,把成员 value 的 refcount 减 1,并且也标为灰色

     3)步骤C (模拟恢复

     在步骤C中,模拟恢复每个紫色变量。当然这个恢复是有条件的,当变量的引用计数大于0时才对其做模拟恢复。同样的每个变量只能恢复一次,恢复后标记为黑色。该步骤基本就是步骤 B 的逆运算。这样剩下的一堆没能恢复的就是该被删除的蓝色节点了,在步骤 D 中遍历出来真的删除掉。

     底层实现:

     重复遍历缓冲区链表,检查当前 value 引用是否为 0,为 0 则表示确实是垃圾,把它标为蓝色 (GC_BLUE)如果不为 0 则排除了引用全部来自自身成员的可能,表示还有外部的引用,并不是垃圾这时候因为步骤 (1) 对成员进行了 refcount 减 1 操作,需要再还原回去,对所有成员进行深度遍历,把成员 refcount 加 1,同时标为黑色

     4)步骤D  (删除蓝色节点

     将步骤C中蓝色的节点遍历出来删除掉。

     底层实现:

     再次遍历缓冲区链表,将非 GC_BLUE 的节点从 roots 链表中移出,最终 roots 链表中全部为真正的垃圾,最后将这些垃圾清除。

     5、GC算法与PHP的集成

     在php中垃圾回收机制默认是打开的,在你的php.ini中可以手动设置,通过zend.enable_gc这个属性进行开启或关闭垃圾回收机制。

     当开启了垃圾回收机制后,每当根缓存区存满时,就会执行上面描述的循环查找算法。根缓存区具有固定的大小,当然你可以通过修改php源码文件Zend/zend_gc.c中常量GC_ROOT_BUFFER_MAX_ENTRIES来修改根缓存区的大小(注意修改后需要重新编译php)。

     当关闭垃圾回收机制后,这个循环查找算法将不会执行,然而可能根会一直存在于根缓冲区中,不管在配置中是否激活了垃圾回收机制。

     当然你也可以通过调用gc_enable()和gc_disable()函数来打开和关闭垃圾回收机制,效果和修改配置项相同。即使根缓冲区还没有满,也能强制执行周期回收。

      6、总结      

      1)PHP 5.3 版本之前

      不包括垃圾回收机制,也没有专门的垃圾回收器,实现垃圾回收就是简单判断一下变量的zval的refcount是否为0,是的话就释放。

      但是如果这么简单的判断垃圾回收的话,很容易引起程序过程中内存溢出。如果存在像2例子中的环形(循环)引用,这种"自身指向自身"的情况的话,那么变量将无法回收造成内存泄露,所以从php5.3开始就出现了专门负责清理垃圾数据防止内存泄露的垃圾回收器。

     2)在 5.3 版本之后,

     做了这些优化:

  •   并不是每次引用计数减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收;
  •   可以解决循环引用问题;
  •   可以总将内存泄露保持在一个阈值以下

     PHP7与PHP5的GC机制不同的地方

     PHP7在垃圾回收机制上做了优化

     想要深入的理解这块,对于没有太多C语言基础的我来说有点难,因为这里面涉及到php底层C实现的过程,所以这里参考了下文章《PHP7垃圾回收机制浅析

1 <?php
2 $a = "new String";
3 xdebug_debug_zval( 'a' );
4 
5 php5.6下输出
6 a: (refcount=1, is_ref=0)='new string'
7 
8 php7.2下输出
9 a: (interned, is_ref=0)='new String' //不使用引用计数的字符串类型被叫做“interned string(保留字符串)

    关于垃圾回收的小知识点

    1)unset():unset()只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数减1,内存是否回收主要还是看refcount是否到0了。

    2)null:将null赋值给一个变量是直接将该变量指向的数据结构置空,同时将其引用计数归0。

    3)脚本执行结束:该脚本中所有内存都会被释放,无论是否有环引用。

 

参考链接:

http://docs.php.net/manual/zh/features.gc.refcounting-basics.php

https://www.jianshu.com/p/d73b3ca418b0

https://learnku.com/articles/33451

 

 

 

posted @ 2020-07-27 14:57  欢乐豆123  阅读(975)  评论(0编辑  收藏  举报