机制介绍
PHP 使用了引用计数(reference counting)GC机制
,同时使用根缓冲区机制
,当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏
问题。
循环引用造成的内存泄漏, 我们为了清理这些垃圾,引入了两个准则:
- 如果引用计数减少到零,所在变量容器将被清除(free),不属于垃圾
- 如果一个zval的引用计数减少后还大于0,那么它会进入垃圾周期。其次,在一个垃圾周期中,通过检查引用计数是否减1,并且检查哪些变量容器的引用次数是零,来发现哪部分是垃圾
每个对象都内含一个引用计数器 refcount,每个reference连接到对象,计数器加1。当reference离开生存空间或被设为 NULL,计数器减1。当某个对象的引用计数器为零时,PHP知道你将不再需要使用这个对象,释放其所占的内存空间。
GC处理完整流程图
机制概念
垃圾回收是一个多数编程语言中都带有的内存管理机制。与非托管性语言相反:C、 C++ 和 Objective C,用户需要手动收集内存,带有 GC 机制的语言:Java、 javaScript 和 PHP 可以自动管理内存。
垃圾回收机制(gc)顾名思义,就是废物重利用的意思,是一种动态存储分配
的方案。它会自动释放程序不再需要的已分配的内存块。垃圾回收机制可以让程序员不必过分关心程序内存分配,从而将更多的精力投入到业务逻辑。
在现在的流行各种语言当中,垃圾回收机制是新一代语言所共有的特征,如Python、PHP、C#、Ruby 等都使用了垃圾回收机制。
回收原理
在PHP5.3版本之前,使用的垃圾回收机制是单纯的“引用计数”。
什么叫做引用计数?
由于PHP是用C来写的,C里面有一种东西叫做结构体
,我们PHP的变量在C中就是用这种方式存储的。
每个PHP的变量都存在于一个叫做zval容器
中,一个zval容器,除了包含变量名和值,还包括两个字节的额外信息:
● 一个叫做is_ref
,是个布尔值,用来表示这个变量是否属于引用集合,通过这个字节,我们php才能把普通变量和引用变量区分开来。
● 第二个额外字节就是refcount
,用来表示指向这个容器的变量的个数。
怎么理解呢? 即:
① 每个内存对象都分配一个计数器,当内存对象被变量引用时,计数器+1
② 当变量引用撤掉后(执行 unset 后),计数器-1
③ 当计数器=0时,表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成
并且PHP在一个生命周期结束后就会释放此进程/线程所占的内容,这种方式决定了PHP在前期不需要过多考虑内存的泄露问题。
但是当两个或多个对象互相引用形成环状后,内存对象的计数器则不会消减为0;这时候,这一组内存对象已经没用了,但是不能回收,从而导致内存泄露的现象。
php5.3开始,使用了新的垃圾回收机制,在引用计数基础上,实现了一种复杂的算法,来检测内存对象中引用环的存在,以避免内存泄露。
随着PHP的发展,PHP开发者的增加以及其所承载的业务范围的扩大,在PHP5.3中引入了更加完善的垃圾回收机制,新的垃圾回收机制解决了无法处理循环的引用内存泄漏问题。
官方文档所说,可以使用Xdebug
来检查引用计数情况:
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );
// 以上例程会输出:
/**
* a: (refcount=3, is_ref=0)='new string'
* a: (refcount=1, is_ref=0)='new string'
**/
注意:从PHP7的NTS版本开始,以上例子的引用将不再被计数,即$c=$b=$a之后a的引用计数也是,具体分类如下
在PHP7中,zval可以被引用计数或不被引用。在zval结构中有一个标志确定了这一点
① 对于
null
,bool
,int
和double
的类型变量,refcount 永远不会计数
② 对于对象、资源类型,refcount计数和php5的一致
③ 对于字符串,未被引用的变量被称为“实际字符串”。而那些被引用的字符串被重复删除(即只有一个带有特定内容的被插入的字符串)并保证在请求的整个持续时间内存在,所以不需要为它们使用引用计数;如果使用了opcache,这些字符串将存在于共享内存中,在这种情况下,您不能使用引用计数(因为我们的引用计数机制是非原子的)
④ 对于数组,未引用的变量被称为“不可变数组”。其数组本身计数与php5一致,但是数组里面的每个键值对的计数,则按前面三条的规则(即如果是字符串也不在计数);如果使用opcache,则代码中的常量数组文字将被转换为不可变数组
再次,这些生活在共享内存,因此不能使用 refcounting。
让我们看下例子来理解吧
<?php
echo '测试字符串引用计数';
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
unset( $b);
xdebug_debug_zval( 'a' );
$b = &$a;
xdebug_debug_zval( 'a' );
echo '测试数组引用计数';
$c = array('a','b');
xdebug_debug_zval( 'c' );
$d = $c;
xdebug_debug_zval( 'c' );
$c[2]='c';
xdebug_debug_zval( 'c' );
echo '测试int型计数';
$e = 1;
xdebug_debug_zval( 'e' );
输出如下:
回收周期
默认的,PHP的垃圾回收机制是打开的,然后有个 php.ini
设置允许你修改它:zend.enable_gc
。
当垃圾回收机制打开时,算法会判断每当根缓存区存满时,就会执行循环查找。根缓存区有固定的大小,默认10,000,可以通过修改PHP源码文件 Zend/zend_gc.c
中的常量GC_ROOT_BUFFER_MAX_ENTRIES
,然后重新编译PHP,来修改这个值。当垃圾回收机制关闭时,循环查找算法永不执行,然而,根将一直存在根缓冲区中,不管在配置中垃圾回收机制是否激活。
除了修改配置 zend.enable_gc ,也能通过分别调用 gc_enable()
和 gc_disable()
函数在运行PHP时来打开和关闭垃圾回收机制。调用这些函数,与修改配置项来打开或关闭垃圾回收机制的效果是一样的。即使在可能根缓冲区还没满时,也能强制执行周期回收。你能调用 gc_collect_cycles()
函数达到这个目的。这个函数将返回使用这个算法回收的周期数。
允许打开和关闭垃圾回收机制并且允许自主的初始化的原因,是由于你的应用程序的某部分可能是高时效性的。在这种情况下,你可能不想使用垃圾回收机制。当然,对你的应用程序的某部分关闭垃圾回收机制,是在冒着可能内存泄漏的风险,因为一些可能根也许存不进有限的根缓冲区。
因此,就在你调用 gc_disable()
函数释放内存之前,先调用 gc_collect_cycles()
函数可能比较明智。因为这将清除已存放在根缓冲区中的所有可能根,然后在垃圾回收机制被关闭时,可留下空缓冲区以有更多空间存储可能根。
性能影响
- 内存占用空间的节省
首先,实现垃圾回收机制的整个原因是为了一旦先决条件满足,通过清理循环引用的变量来节省内存占用。在PHP执行中,一旦根缓冲区满了或者调用 gc_collect_cycles() 函数时,就会执行垃圾回收。
- 执行时间增加
垃圾回收影响性能的第二个领域是它释放已泄漏的内存耗费的时间。
通常,PHP中的垃圾回收机制,仅仅在循环回收算法确实运行时会有时间消耗上的增加。但是在平常的(更小的)脚本中根本就没有性能影响。
- 在平常脚本中有循环回收机制运行的情况下,内存的节省将允许更多这种脚本同时运行在你的服务器上。因为总共使用的内存没达到上限
这种好处在长时间运行脚本中尤其明显,诸如长时间的测试套件或者daemon脚本此类。同时,对通常比Web脚本运行时间长的脚本应用程序,新的垃圾回收机制,应该会大大改变一直以来认为内存泄漏问题难以解决的看法。
小结
unset
: 只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数减1,内存是否回收主要还是看refcount是否到0了null
: 将 null 赋值给一个变量是直接将该变量指向的数据结构置空,同时将其引用计数归0脚本执行结束
: 该脚本中所有内存都会被释放,无论是否有环引用
评论一下?