PHPER必了解——PHP GC机制
前言
- PHP5.3之前(不包括5.3)的垃圾回收机制,是没有专门的垃圾回收器。
只是简单的判断了一下变量的zval的refcount是否为0,是的话就释放否则不释放直至进程结束。 - PHP5.3出现了专门负责清理垃圾数据、防止内存泄漏的GC。
PHP变量 zval 容器
每个php变量存在一个叫"zval"的变量【zval容器】中
name:字段值
is_ref:标识这个变量是否是属于引用集合变量
refcount:表示指向这个zval变量容器的变量(也称符号即symbol)个数
当被变量引用时refcount+1,当变量撤掉时refcount-1,当计数器=0时,表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成。
变量引用示例
- refcount计数减1,unset并非一定会释放内存,当有两个变量指向的时候,并非会释放变量占用的内存,只是refcount减1
【变量销毁】
$name = '张三';
xdebug_debug_zval('name');
$test = &$name;
xdebug_debug_zval('name');
unset($test);
xdebug_debug_zval('name');
//结果如下:
name:
(refcount=1, is_ref=0)string '张三' (length=6)
name:
(refcount=2, is_ref=0)string '张三' (length=6)
name:
(refcount=1, is_ref=0)string '张三' (length=6)
【环行引用】php变量的回收机制只是简单的通过计数来处理(当refcount=0时,会回收内存),但这样会出现一个问题(老版本会导致内存泄漏)
$a = ['one']; //--- step0
xdebug_debug_zval('a');
//结果
a:
(refcount=2, is_ref=0)
array (size=1)
0 => (refcount=1, is_ref=0)string 'one' (length=3)
$a[] = &$a; // --- step1
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<
unset($a); //--- step2
xdebug_debug_zval('a');
//结果
a: no such symbol
- step1执行unset之前,$a的refcount 为2,
- 执行unset之后,$a的refcout为1
- 因为是1不等于0,不能被回收内存,即为垃圾
- 当然,在php脚本执行完毕后,所分配的内存将全部被回收(php-fpm模式)
- 但是现在php除了应用于脚本以外,更多的地方用于写守护服务(swoole),可能很久(比如一年)才结束脚本,这期间例如上面的程序会产生内存溢出
注意:unset并不能释放内存,需要看zval的refcount是否为0(敲黑板)
潜在的内存变化
print_r(memory_get_usage());
for($i=0;$i<100;$i++)
{
$a = "test".$i;
$$a = "temp";
}
print_r(memory_get_usage());
for($i=0;$i<100;$i++)
{
$a = "test".$i;
unset($$a);
}
print_r(memory_get_usage());
结果:
2006640
2017392
2014224
内存没有全部回收回来!
对于php的核心结构Hashtable来说,由于未知性,定义的时候不可能一次性分配足够多的内存块。
所以初始化的时候只会分配一小块,等不够的时候在进行扩容,而Hashtable只扩容不减少,所以就出现了上述的情况:
1.当存入100个变量的时候,符号表不够用了就进行一次扩容
2.当unset的时候只释放了”为变量值分配内存”,而“为变量名分配内存”是在符号表的,符号表并没有缩小
3.所以没收回来的内存是被符号表占去了。
1.php的内存申请: php并非简单的向os申请内存,而是会申请一大块内存,把其中一部分分给申请者,这样当再有逻辑来申请内存的时候,就不需要向os申请了,避免了频繁调用。当内存不够的时候才会再次申请
2.php的内存释放:当释放内存的时候,php并非会把内存还给os,而是把内存轨道自己维护的空闲内存列表,以便重复利用
GC垃圾回收机制
基本准则:
- 1.如果一个zval的refcount增加,那么此zval还在使用,不属于垃圾
- 2.如果一个zval的refcount减少到0, 那么zval可以被释放掉,不属于垃圾
- 3.如果一个zval的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾
php官方手册的配图
图解
A:为了避免每次变量的refcount减少的时候都调用GC的算法进行垃圾判断,此算法会先把所有前面准则3情况下的zval节点放入一个节点(root)缓冲区【root buffer】,并且将这些zval节点标记成紫色,同时算法必须确保每一个zval节点在缓冲区中之出现一次。当缓冲区被节点塞满的时候,GC才开始开始对缓冲区中的zval节点进行垃圾判断。
B:当缓冲区满了之后,算法以深度优先对每一个节点所包含的zval进行减1操作,为了确保不会对同一个zval的refcount重复执行减1操作,一旦zval的refcount减1之后会将zval标记成灰色。需要强调的是,这个步骤中,起初节点zval本身不做减1操作,但是如果节点zval中包含的zval又指向了节点zval(环形引用),那么这个时候需要对节点zval进行减1操作。
C:算法再次以深度优先判断每一个节点包含的zval的值,如果zval的refcount等于0,那么将其标记成白色(代表垃圾),如果zval的refcount大于0,那么将对此zval以及其包含的zval进行refcount加1操作,这个是对非垃圾的还原操作,同时将这些zval的颜色变成黑色(zval的默认颜色属性)
D:遍历zval节点,将C中标记成白色的节点zval释放掉。
相关函数
缓存区达到临界值时,遍历删除是垃圾的值
-
__destruct() 析构函数,是在垃圾对象被回收时执行。
-
unset 销毁的是指向对象的变量,而不是这个对象。
-
$a=null 将变量指向空容器