php垃圾回收算法分代,PHP实战之CG算法及垃圾回收机制

1.前言

面试的时候,经常会被问到什么是php的垃圾回收机制?下面我们从浅到深逐步了解。

2.基本概念

2.1xdebug_debug_zval()函数

要想直观的了解垃圾回收机制,需要借助xdebug的xdebug_debug_zval()函数,即是说需要安装xdebug扩展,下面是我们需要了解xdebug_debug_zval()函数展示的一些变量含义。

refcount:多少个变量是用了一样的相同值,这个数值就是多少。

is_ref:bool类型,当refcount大于2的时候,其中一个变量用了地址&的形式进行赋值,好了,它就变成1了。

2.1.1基本内部结构

$name = "FZ杰哥";

xdebug_debug_zval('name');

得到:

name:(refcount=1, is_ref=0),string 'FZ杰哥' (length=10)

2.1.2增加一个计数

$name = "FZ杰哥";

$temp_name = $name;

xdebug_debug_zval('name');

得到:

name:(refcount=2, is_ref=0),string 'FZ杰哥' (length=10)

此时refcount由于$temp_name使用了相同值,所以+1

2.1.3增加一个引用赋值

$name = "FZ杰哥";

$temp_name = &$name;

xdebug_debug_zval('name');

得到:

name:(refcount=2, is_ref=1),string 'FZ杰哥' (length=10)

引用赋值会导致zval通过is_ref来标记是否存在引用的情况。

2.1.4数组型的变量

$name = ['a'=>'FZ', 'b'=>'杰哥'];

xdebug_debug_zval('name');

得到:

name:(refcount=1, is_ref=0),array (size=2)

'a' => (refcount=1, is_ref=0),string ''FZ'' (length=4)

'b' => (refcount=1, is_ref=0),string '杰哥' (length=6)

2.1.5销毁变量

$name = "杰哥";

$temp_name = $name;

xdebug_debug_zval('name');

unset($temp_name);

xdebug_debug_zval('name');

得到:

name:(refcount=2, is_ref=0),string '杰哥' (length=6)

name:(refcount=1, is_ref=0),string ''杰哥' (length=6)

可以观察到,unset()并不一定会释放内存,当有两个变量指向的时候,并非会释放变量占用的内存,只是refcount减1。

3.php的内存管理机制

3.1memory_get_usage()函数获取外在的内存变化

//获取内存方法,加上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

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

3.2memory_get_usage()函数获取潜在的内存变化

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

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

我们会发现,内存没有完全收回,其原因是,当unset的时候只释放了”为变量值分配内存”,而“为变量名分配内存”是在符号表的,符号表并没有缩小,所以没收回来的内存是被符号表占去了。

4.php如何定义垃圾

php中判断是否为垃圾,主要看有没有变量名指向变量容器zval,如果没有则认为是垃圾,从而释放。(即表示当refcount=0时候释放内存)

4.1第一代:php5.3之前的版本

首先,在php5.3(不包括5.3)之前的版本是没有专门的垃圾回收器的,是通过对变量zval的refcount判断是否为0,为0就释放内存否则就不释放直到进程结束。但这样的处理会导致内存溢出的问题,无法回收的内存造成了内存泄漏。

产生内存溢出的原因是:环形引用

代码举例:

$a = ['one'];

$a[] = &$a;

xdebug_debug_zval('a');

?>

得到xdebug如下:

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自身的引用。

图示:

1de98ab1c0cfd19f1bd597b60b96f5b6.png

如果此时删掉$a:

$a = ['one'];

$a[] = &$a;

unset($a);

就会形成环形引用,如下图:

d73ed44fbba3ca710a2761d7be5f8907.png

因此在小于php5.3的版本就会出现一个问题:$a已经不在符号表了,没有变量再指向此zval容器,用户已无法访问,但是由于数组的refcount变为1而不是0,导致此部分内存不能被回收从而产生了内存泄漏。所以PHP5.3出现了专门负责清理垃圾数据、防止内存泄漏的GC。

4.2第二代:php5.3之后的版本(含5.3)

php5.3之后的版本为了解决环形饮用导致的内存溢出问题,产生了新的GC算法,遵守以下原则:

(1)如果一个zval的refcount增加,那么此zval还在使用,不属于垃圾

(2)如果一个zval的refcount减少到0, 那么zval可以被释放掉,不属于垃圾

(3)如果一个zval的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾。

如何理解?就是对此zval中的每个元素进行一次refcount减1操作,操作完成之后,如果zval的refcount=0,那么这个zval就可能是一个垃圾。

c8e17eb0ff2df82ccb60304fed265f15.png

A:为了避免每次变量的refcount减少的时候都调用GC的算法进行垃圾判断,此算法会先把所有前面准则3情况下的zval节点放入一个节点(root)缓冲区(root buffer),并且将这些zval节点标记成紫色,同时算法必须确保每一个zval节点在缓冲区中之出现一次。当缓冲区被节点塞满的时候,GC才开始开始对缓冲区中的zval节点进行垃圾判断。

0f274fc2d5d9cc9070007eb3e278faa9.png

B:当缓冲区满了之后,算法以深度优先对每一个节点所包含的zval进行减1操作,为了确保不会对同一个zval的refcount重复执行减1操作,一旦zval的refcount减1之后会将zval标记成灰色。需要强调的是,这个步骤中,起初节点zval本身不做减1操作,但是如果节点zval中包含的zval又指向了节点zval(既是环形引用),那么这个时候需要对节点zval进行减1操作。

1c54d7c001e659df1d10082b7907aa80.png

C:算法再次以深度优先判断每一个节点包含的zval的值,如果zval的refcount等于0,那么将其标记成白色(代表垃圾),如果zval的refcount大于0,那么将对此zval以及其包含的zval进行refcount加1操作,这个是对非垃圾的还原操作,同时将这些zval的颜色变成黑色(zval的默认颜色属性)

317d37deae6d2c641e056a341e158091.png

D:遍历zval节点,将C中标记成白色的节点zval释放掉。

4.3算法的套路

对于一个包含环形引用的数组,对数组中包含的每个元素的zval进行减1操作,之后如果发现数组自身的zval的refcount变成了0,那么可以判断这个数组是一个垃圾。

4.4 GC算法配置与优化

php做事情套路都是走批量的原则。申请内存也是申请一大块,仅使用当前的一小部分剩下的等下回再用,避免多次申请。这个gc算法也是这样,会有一个缓冲区的概念,等缓冲区满了才会一次性去给清掉。

4.5 GC开关配置

php.ini中设置 zend.enable_gc 项来开启或则关闭GC。

4.5.1缓存区配置

缓冲区默认可以放10,000个节点,当缓冲区满了才会清理。可以通过修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 来改变这个数值,关键函数如下:

gc_enable() : 开启GC

gc_disable() : 关闭GC

gc_collect_cycles() : 在节点缓冲区未满的情况下强制执行垃圾分析算法

4.5.2如何触发内存/垃圾回收

unset()函数

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

= null 操作

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

脚本执行结束

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

4.6第三代:php7版本的一些变化

在 PHP7 中 zval 有了新的实现方式。最基础的变化就是 zval 需要的内存不再是单独从堆上分配,不再自己存储引用计数。复杂数据类型(比如字符串、数组和对象)的引用计数由其自身来存储。这样就可以有更少的内存分配操作、更少的间接指针使用以及更少的内存分配。

5.参考文献

详谈PHP垃圾回收机制->https://blog.csdn.net/wangshiqueque/article/details/78074363

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值