1、引用计数基本知识php
每一个php变量存在一个叫"zval"的变量容器中,当一个变量被赋常量值时,就会生成一个zval变量容器。一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。第一个是"is_ref",是个bool值,用来标识这个变量是不是属于引用集合(reference set)。经过这个字节,php引擎才能把普通变量和引用变量区分开来,因为php容许用户经过使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用。第二个额外字节是"refcount",用以表示指向这个zval变量容器的变量(也称符号即symbol)个数。全部的符号存在一个符号表中,其中每一个符号都有做用域(scope),那些主脚本(好比:经过浏览器请求的的脚本)和每一个函数或者方法也都有做用域。算法
当把一个变量赋值给另外一变量将增长引用次数(refcount)。不必时,php不会去复制已生成的变量容器。变量容器在”refcount“变成0时就被销毁. 当任何关联到某个变量容器的变量离开它的做用域(好比:函数执行结束),或者对变量调用了函数 unset()时,”refcount“就会减1。数组
若是你已经安装了Xdebug,你能经过调用函数xdebug_debug_zval()显示"refcount"和"is_ref"的值。浏览器
当考虑像 array和object这样的复合类型时,事情就稍微有点复杂.。与 标量(scalar)类型的值不一样,array和 object类型的变量把它们的成员或属性存在本身的符号表中。缓存
数组和内存示例:服务器
(1)通常状况:数据结构
$a = array( 'meaning' => 'life', 'number' => 42 );函数
(2)数组中有两个元素的值相同:性能
$a = array( 'meaning' => 'life', 'number' => 42 );单元测试
$a['life'] = $a['meaning'];
(3)将数组自己做为本身的一个元素:
$a = array( 'one' );
$a[] =& $a;
执行unset($a);以后:
在第三种状况中,尽管再也不有某个做用域中的任何符号指向这个结构(就是变量容器),因为数组元素“1”仍然指向数组自己,因此这个容器不能被清除 。由于没有另外的符号指向它,用户没有办法清除这个结构,结果就会致使内存泄漏。庆幸的是,php将在脚本执行结束时清除这个数据结构,可是在php清除以前,将耗费很多内存。若是你要实现分析算法,或者要作其余像一个子元素指向它的父元素这样的事情,这种状况就会常常发生。固然,一样的状况也会发生在对象上,实际上对象更有可能出现这种状况,由于对象老是隐式的被引用。
若是上面的状况发生仅仅一两次倒没什么,可是若是出现几千次,甚至几十万次的内存泄漏,这显然是个大问题。这样的问题每每发生在长时间运行的脚本中,好比请求基本上不会结束的守护进程或者单元测试中的大的套件中。后者的例子:在给巨大的eZ(一个知名的PHP Library) 组件库的模板组件作单元测试时,就可能会出现问题。有时测试可能须要耗用2GB的内存,而测试服务器极可能没有这么大的内存。
2、回收周期
PHP5.3以前使用的垃圾回收机制是单纯的“引用计数”,也就是为每一个内存对象都分配一个引用计数器,这样的机制存在一个问题,就是当两个或多个对象互相引用造成环状时,会出现内存泄露现象。PHP5.3开始,使用了新的垃圾回收机制,在引用计数的基础上,实现了一种复杂的算法,来检测内存对象中引用环的存在,以免内存泄露。
算法的一些基本规则:若是一个引用计数增长,它将继续被使用,固然就再也不在垃圾中。若是引用计数减小到零,所在变量容器将被清除。就是说,仅仅在引用计数减小到非零值时,才会产生垃圾周期。其次,在一个垃圾周期中,经过检查引用计数是否减1,而且检查哪些变量容器的引用次数是零,来发现哪部分是垃圾。
为避免不得不检查全部引用计数可能减小的垃圾周期,这个算法把全部可能根(都是zval变量容器),放在根缓冲区中(用紫色来标记,称为疑似垃圾),这样能够同时确保每一个可能的垃圾根在缓冲区中只出现一次。仅仅在根缓冲区满了时,才对缓冲区内部全部不一样的变量容器执行垃圾回收操做。看上图的步骤 A。
在步骤 B 中,模拟删除每一个紫色变量。模拟删除时可能将不是紫色的普通变量引用数减"1",若是某个普通变量引用计数变成0了,就对这个普通变量再作一次模拟删除。每一个变量只能被模拟删除一次,模拟删除后标记为灰色。
在步骤 C 中,模拟恢复每一个紫色变量。恢复是有条件的,当变量的引用计数大于0时才对其作模拟恢复。一样每一个变量只能恢复一次,恢复后标记为黑,基本就是步骤 B 的逆运算(将普通变量引用数加"1")。这样剩下的一堆没能恢复的就是该删除的蓝色节点了,在步骤 D 中遍历出来真的删除掉。
算法中都是模拟删除、模拟恢复、真的删除,都使用简单的遍历便可(最典型的深搜遍历)。复杂度为执行模拟操做的节点数正相关,不仅是紫色的那些疑似垃圾变量。
默认的,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()函数可能比较明智。由于这将清除已存放在根缓冲区中的全部可能根,而后在垃圾回收机制被关闭时,可留下空缓冲区以有更多空间存储可能根。
3、性能方面考虑的因素
相比较于PHP5.2,PHP5.3采用的这种垃圾回收机制对性能的影响主要体如今两个方面上:第一是内存占用空间的节省,第二是垃圾回收机制执行内存清理时的执行时间增长。
一般,PHP中的垃圾回收机制,仅仅在循环回收算法确实运行时会有时间消耗上的增长。可是在日常的(更小的)脚本中应根本就没有性能影响。
然而,在日常脚本中有循环回收机制运行的状况下,内存的节省将容许更多这种脚本同时运行在你的服务器上。由于总共使用的内存没达到上限。这种好处在长时间运行脚本中尤为明显,诸如长时间的测试套件或者daemon脚本此类。