PHP垃圾回收机制(GC)
PHP垃圾回收机制(GC)
前言
垃圾回收是一个多数编程语言中都带有的内存管理机制。与非托管性语言相反:C, C++ 和 Objective C,用户需要手动收集内存,带有 GC 机制的语言:Java, javaScript 和 PHP 可以自动管理内存。
下面是阅读 《官方手册-垃圾回收机制》后,根据我自己的理解做的笔记。方便日后查阅。
1、引用计数的基本知识
我们要了解GC,那么首先要了解引起垃圾回收的计数是什么。
在php中,每个变量存在一个叫“zval”的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括另外两个字节的额外信息:is_ref和refcount。
当一个变量被赋常量值时,就会生成一个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