什么是PHP内存回收机制?在了解PHP回收机制之前,我觉得大家有必要先了解PHP垃圾回收机制。首先我们来了解下,什么是垃圾回收机制?是的,平时经常听到大牛说到的gc,就是垃圾回收器,全称Garbage Collection。

早期版本,准确地说是5.3之前(不包括5.3)的垃圾回收机制,是没有专门的垃圾回收器的。只是简单的判断了一下变量的zval的refcount是否为0,是的话就释放否则不释放直至进程结束。

乍一看确实没毛病啊,然而其中隐藏着变量内存溢出的风险,无法回收的内存造成了内存泄漏,所以PHP5.3出现了专门负责清理垃圾数据、防止内存泄漏的GC。下文将由浅入深(凭感觉)来记录下php的垃圾回收机制是怎么一回事?

1.php引用计数基本知识点

每个php变量存在一个叫"zval"的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。第一个是"is_ref",是个bool值,用来标识这个变量是否是属于引用集合(reference set)。通过这个字节,php引擎才能把普通变量和引用变量区分开来,由于php允许用户通过使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用。第二个额外字节是"refcount",用以表示指向这个zval变量容器的变量(也称符号即symbol)个数。所有的符号存在一个符号表中,其中每个符号都有作用域(scope),那些主脚本(比如:通过浏览器请求的的脚本)和每个函数或者方法也都有作用域。

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

Example #1 生成一个新的zval容器

<?php
$a = "new string";
?>

在上例中,新的变量a,是在当前作用域中生成的。并且生成了类型为 string 和值为new string的变量容器。在额外的两个字节信息中,"is_ref"被默认设置为 FALSE,因为没有任何自定义的引用生成。"refcount" 被设定为 1,因为这里只有一个变量使用这个变量容器. 注意到当"refcount"的值是1时,"is_ref"的值总是FALSE. 如果你已经安装了» Xdebug,你能通过调用函数 xdebug_debug_zval()显示"refcount"和"is_ref"的值。

Example #2 显示zval信息

<?php
xdebug_debug_zval('a');
?>

以上例程会输出:

a: (refcount=1, is_ref=0)='new string'

把一个变量赋值给另一变量将增加引用次数(refcount).

Example #3 增加一个zval的引用计数

<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>

以上例程会输出:

a: (refcount=2, is_ref=0)='new string'

这时,引用次数是2,因为同一个变量容器被变量 a 和变量 b关联.当没必要时,php不会去复制已生成的变量容器。变量容器在”refcount“变成0时就被销毁. 当任何关联到某个变量容器的变量离开它的作用域(比如:函数执行结束),或者对变量调用了函数 unset()时,”refcount“就会减1,下面的例子就能说明:

Example #4 减少引用计数

<?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'

如果我们现在执行 unset($a);,包含类型和值的这个变量容器就会从内存中删除。

2.php的内存管理机制

知道了zval是怎么一回事,接下来看看如何通过php直观看到内存管理的机制是怎么样的。

外在的内存变化

先来一段代码:

<?php
   //获取内存方法,加上true返回实际内存,不加则返回表现内存
   var_dump(memory_get_usage());
   $name = "咖啡色的羊驼";
   var_dump(memory_get_usage());
   unset($name);
   var_dump(memory_get_usage());

会得到:

int 1593248
int 1593384
int 1593248

大致过程:定义变量->内存增加->清除变量->内存恢复

潜在的内存变化

当执行:$name = "咖啡色的羊驼";

时候,内存的分配做了两件事情:1.为变量名分配内存,存入符号表 2.为变量值分配内存

再来看代码:

<?php

   var_dump(memory_get_usage());
   for($i=0;$i<100;$i++)
   {
       $a = "test".$i;
       $$a = "hello";    
}
var_dump(memory_get_usage());
for($i=0;$i<100;$i++)
{
   $a = "test".$i;
unset($$a);    
   }
   var_dump(memory_get_usage());

会得到:

int 1596864
int 1612080
int 1597680

简直爆炸,怎么和之前看的不一样?内存没有全部回收回来。

对于php的核心结构Hashtable来说,由于未知性,定义的时候不可能一次性分配足够多的内存块。所以初始化的时候只会分配一小块,等不够的时候在进行扩容,而Hashtable只扩容不减少,所以就出现了上述的情况:当存入100个变量的时候,符号表不够用了就进行一次扩容,当unset的时候只释放了”为变量值分配内存”,而“为变量名分配内存”是在符号表的,符号表并没有缩小,所以没收回来的内存是被符号表占去了。

潜在的内存申请与释放设计

php和c语言一样,也是需要进行申请内存的,只不过这些操作作者都封装到底层了,php使用者无感知而已。

php的内存申请小设计

php并非简单的向os申请内存,而是会申请一大块内存,把其中一部分分给申请者,这样当再有逻辑来申请内存的时候,就不需要向os申请了,避免了频繁调用。当内存不够的时候才会再次申请

php的内存释放小设计

当释放内存的时候,php并非会把内存还给os,而是把内存轨道自己维护的空闲内存列表,以便重复利用,

3.php中垃圾是如何定义的?

准确地说,判断是否为垃圾,主要看有没有变量名指向变量容器zval,如果没有则认为是垃圾,需要释放。

打个比方:

<?php
   $name = "咖啡色的羊驼";
   // todo other things

当定义name的时候,处理完字符串准备做其他事情的时候,对于我们来说name的时候,处理完字符串准备做其他事情的时候,对于我们来说name就是可以回收的垃圾了,然而对于引擎来说,$name还是实打实存在的refcount也还是1,所以就不是垃圾,不能回收。当调用unset的时候,也并不一定引擎会认为它是一个垃圾而进行回收,主要还是看refcount是不是真的变为0了。

4.老版本php中如何产生内存泄漏?

产生内存泄漏主要真凶:环形引用。 

现在来造一个环形引用的场景:

<?php
   $a = ['one'];
   $a[] = &$a;
   xdebug_debug_zval('a');

得到:

a:
(refcount=2, is_ref=1),
array (size=2)
 0 => (refcount=1, is_ref=0),string 'one' (length=3)
 1 => (refcount=2, is_ref=1),
       &array<

这样 $a数组就有了两个元素,一个索引为0,值为one字符串,另一个索引为1,为$a自身的引用。

此时删掉$a:

<?php
   $a = ['one'];
   $a[] = &$a;
   unset($a);

如果在小于php5.3的版本就会出现一个问题:$a已经不在符号表了,没有变量再指向此zval容器,用户已无法访问,但是由于数组的refcount变为1而不是0,导致此部分内存不能被回收从而产生了内存泄漏。

5.5.3版本前后php是如何处理垃圾内存的?

1.在5.2版本或之前版本,PHP会根据refcount值来判断是不是垃圾

如果refcount值为0,PHP会当做垃圾释放掉

这种回收机制有缺陷,对于环状引用的变量无法回收

2.在5.3之后版本改进了垃圾回收机制

如果发现一个zval容器中的refcount在增加,说明不是垃圾

如果发现一个zval容器中的refcount在减少,如果减到了0,直接当做垃圾回收

如果发现一个zval容器中的refcount在减少,并没有减到0,PHP会把该值放到缓冲区,当做有可能是垃圾的怀疑对象。

当缓冲区达到了临界值,PHP会自动调用一个方法去遍历每一个值,如果发现是垃圾就清理

6.涉及到垃圾回收的知识点

1.unset函数

unset只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数-1;内存是否回收主要还是看refount是否到0了,以及gc算法判断。

2.= null 操作;

a=null是直接将a=null是直接将a 指向的数据结构置空,同时将其引用计数归0。

3.脚本执行结束

脚本执行结束,该脚本中使用的所有内存都会被释放,不论是否有引用环。

最后在哪里可以设置这个回收机制呢?默认的,PHP的垃圾回收机制是打开的,然后在配置文件 php.ini 里允许你修改它:zend.enable_gc 。除了修改配置zend.enable_gc是否开启 ,也能通过分别调用gc_enable() 和 gc_disable()函数来打开和关闭垃圾回收机制。调用这些函数,与修改配置项来打开或关闭垃圾回收机制的效果是一样的。如果想在根缓冲区还没满时强制执行周期回收,可以调用gc_collect_cycles()函数,这个函数将返回使用这个算法回收的周期数。

当垃圾回收机制打开时,每当根缓存区存满时,就会执行查找算法。根缓存区有固定的大小,可存10,000个可能根,当然可以通过修改PHP源码文件Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然后重新编译PHP,来修改这个10,000值。当垃圾回收机制关闭时,循环查找算法永不执行,然而,可能根将一直存在根缓冲区中,不管在配置中垃圾回收机制是否激活。

当垃圾回收机制关闭时,如果根缓冲区存满了可能根,更多的可能根显然不会被记录。那些没被记录的可能根,将不会被这个算法来分析处理。如果他们是循环引用周期的一部分,将永不能被清除进而导致内存泄漏。

即使在垃圾回收机制不可用时,可能根也被记录的原因是,相对于每次找到可能根后检查垃圾回收机制是否打开而言,记录可能根的操作更快。不过垃圾回收和分析机制本身要耗不少时间。

也就是说关闭了回收机制也会往缓冲区丢疑似垃圾的容器,当缓冲区满了的时候不会执行回收算法,后面更多的疑似垃圾容器不会继续放进去,就可能导致内存泄漏。当开启回收机制后,就会从缓冲区中之前放入的容器中开始垃圾回收机制。