PHP垃圾回收机制是使用了引用计数
每个php变量存在一个叫"zval"的变量容器中,当一个变量被赋常量值时,就会生成一个zval变量容器,zval变量容器,除了包含变量名和值,还包括2个字节的额外信息,一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。
- is_ref:表示这个变量是否属于引用集合,用来区分普通变量和引用变量,php允许用户通过使用&来使用自定义引用
- refcount:表示指向这个zval变量容器的变量(也称符号即symbol)个数
<?php
$a = 1;
xdebug_debug_zval('a'); // a: (refcount=0, is_ref=0)=1
$b = &$a;
xdebug_debug_zval('a'); // a: (refcount=2, is_ref=1)=1
$c = &$b;
xdebug_debug_zval('a'); // a: (refcount=3, is_ref=1)=1
unset($a);
xdebug_debug_zval('a'); // a: no such symbol
xdebug_debug_zval('b'); // b: (refcount=2, is_ref=1)=1
array和 object类型的变量把它们的成员或属性存在自己的符号表中。这意味着下面的例子将生成3个zval变量容器
<?php
$a = [
'name' => 'php',
'year' => 27
];
xdebug_debug_zval('a');
// a: (refcount=2, is_ref=0)=array ('
// name' => (refcount=1, is_ref=0)='php',
// 'year' => (refcount=0, is_ref=0)=27
// )
循环引用内存泄漏问题处理:
首先,我们先要建立一些基本规则,
- 如果一个引用计数增加,它将继续被使用,当然就不再在垃圾中。
- 如果引用计数减少到零,所在变量容器将被清除(free)。就是说,仅仅在引用计数减少到非零值时,才会产生垃圾周期(garbage cycle)。
其次,在一个垃圾周期中,通过检查引用计数是否减1,并且检查哪些变量容器的引用次数是零,来发现哪部分是垃圾。
为避免不得不检查所有引用计数可能减少的垃圾周期,这个算法把所有可能根(possible roots 都是zval变量容器),放在根缓冲区(root buffer)中(用紫色来标记,称为疑似垃圾),这样可以同时确保每个可能的垃圾根(possible garbage root)在缓冲区中只出现一次。仅仅在根缓冲区满了时,才对缓冲区内部所有不同的变量容器执行垃圾回收操作。看上图的步骤 A。
在步骤 B 中,模拟删除每个紫色变量。模拟删除时可能将不是紫色的普通变量引用数减"1",如果某个普通变量引用计数变成0了,就对这个普通变量再做一次模拟删除。每个变量只能被模拟删除一次,模拟删除后标记为灰(原文说确保不会对同一个变量容器减两次"1",不对的吧)。
在步骤 C 中,模拟恢复每个紫色变量。恢复是有条件的,当变量的引用计数大于0时才对其做模拟恢复。同样每个变量只能恢复一次,恢复后标记为黑,基本就是步骤 B 的逆运算。这样剩下的一堆没能恢复的就是该删除的蓝色节点了,在步骤 D 中遍历出来真的删除掉。
算法中都是模拟删除、模拟恢复、真的删除,都使用简单的遍历即可(最典型的深搜遍历)。复杂度为执行模拟操作的节点数正相关,不只是紫色的那些疑似垃圾变量。
循环查找算法与PHP集成
默认的,PHP的垃圾回收机制是打开的,然后有个 php.ini 设置允许你修改它:zend.enable_gc。
当垃圾回收机制打开时,每当根缓存区存满时,就会执行上面描述的循环查找算法。根缓存区有固定的大小,可存10,000个可能根,当然你可以通过修改PHP源码文件Zend/zend_gc.c
中的常量GC_ROOT_BUFFER_MAX_ENTRIES
,然后重新编译PHP,来修改这个10,000值。当垃圾回收机制关闭时,循环查找算法永不执行,然而,可能根将一直存在根缓冲区中,不管在配置中垃圾回收机制是否激活。
当垃圾回收机制关闭时,如果根缓冲区存满了可能根,更多的可能根显然不会被记录。那些没被记录的可能根,将不会被这个算法来分析处理。如果他们是循环引用周期的一部分,将永不能被清除进而导致内存泄漏。
即使在垃圾回收机制不可用时,可能根也被记录的原因是,相对于每次找到可能根后检查垃圾回收机制是否打开而言,记录可能根的操作更快。不过垃圾回收和分析机制本身要耗不少时间。
除了修改配置zend.enable_gc,也能通过分别调用gc_enable() 和 gc_disable()函数来打开和关闭垃圾回收机制。调用这些函数,与修改配置项来打开或关闭垃圾回收机制的效果是一样的。即使在可能根缓冲区还没满时,也能强制执行周期回收。你能调用gc_collect_cycles()函数达到这个目的。这个函数将返回使用这个算法回收的周期数。
允许打开和关闭垃圾回收机制并且允许自主的初始化的原因,是由于你的应用程序的某部分可能是高时效性的。在这种情况下,你可能不想使用垃圾回收机制。当然,对你的应用程序的某部分关闭垃圾回收机制,是在冒着可能内存泄漏的风险,因为一些可能根也许存不进有限的根缓冲区。因此,就在你调用gc_disable()函数释放内存之前,先调用gc_collect_cycles()函数可能比较明智。因为这将清除已存放在根缓冲区中的所有可能根,然后在垃圾回收机制被关闭时,可留下空缓冲区以有更多空间存储可能根。