php-fpm 回收,PHP的垃圾回收机制以及大概实现

垃圾回收,简称gc。顾名思义,就是废物重利用的意思。再说这个之前先接触一下内存泄露,大概意思就是申请了一块地儿拉了会儿屎,拉完后不收拾,那么那块儿地就算是糟蹋了,地越用越少,最后一地全是屎。说到底一句,用了记得还。一定程度上说,垃圾回收机制就是用来擦屁股的。如果用过C语言,那么申请内存的方式是malloc或者是calloc,然后你用完这个内存后,一定不要忘了用free函数去释放掉,这就是传说中手动垃圾回收,一般都是扫地神僧用这种方式。很多高层次语言中,你这辈子都是接触不到内存管理的,比如世界上最好的语言php,这种语言替你管理了内存。当然了,如果你用的swoole或者wm或者自己发明的常驻内存级php应用,那你将不得不关注内存泄露问题,也就说一定要记得释放无用变量。那么,在用的最普遍地最传统的web开发中,php的自动垃圾回收机制是怎样的呢?这个问题我们先这么想,就是都知道php是C语言实现的,现在把C语言给你放在这里了,然后你想想如何用C语言实现对一个变量的统计以及释放。你不要想如何实现php,你就想C语言如何实现一个变量,从声明开始到最后没人用了,就把这个变量所占的内存给释放掉。

好了,步入正题,PHP进行内存管理的核心算法一共两项:一是引用计数,二是写时拷贝。当你声明一个PHP变量的时候,C语言就在底层给你搞了一个叫做zval的struct(结构体);如果你还给这个变量赋值了,比如“hello world”,那么C语言就在底层再给你搞一个叫做zend_value的union(联合体)。

好了,进入代码实战阶段,注意两点:

我用的PHP版本是7.1.17(记住!这个很重要!不同版本的PHP有极大可能会出现不相同的结果!我试过6个版本的PHP,三个PHP5版本,三个HPP7版本,其中PHP7版本变化尤其多,但不影响业务代码不会出bug,放心),运行环境是cli。

下面的原理解只针对PHP7,不再说5了。你面试的时候,只需要说5的我不太了解,7的我深入看过一些即可,面试官不会难为你的。

$a = 'hello'.mt_rand( 1, 1000 );

echo xdebug_debug_zval('a');

$b = $a;

echo xdebug_debug_zval('a');

$c = $a;

echo xdebug_debug_zval('a');

unset( $c );

echo xdebug_debug_zval('a');

输出的结果是:

4230015f5ae3

image

其中,zval struct结构体用于保存$a,zend_value union联合体用于保存数据内容也就是'hello916'。由于后面又声明了b和c,所以C不得不又在底层给你搞出两个zval struct结构体来。

其中,zval和zend value的结构大概如下:(注意!!!这并不是完整正确的PHP zval和zend_value在C语言中struct和union实现,仅仅是挑出最重点的部分写出来,强调一下:你没有必要一个字不差背诵过zval和zend_value,你只需要知道原理)

zval {

string "a" //变量的名字是a

value zend_value //变量的值

type string //变量是字符串类型 }

zend_value {

string "hello916" //值的内容

refcount 1 //引用计数 }

看到上面两个,如果面试官问你php变量为什么能够保存字符串"123"也能保存数字123,你知道该怎么回答了吧?就答出重点zval中有该变量的类型,当是字符串123的时候,type就是string,此时value指向“123”;当是整数123的时候,zval的type为int,value为123。这就是答题的思想,这很重要!而且,通过C语言都是可以实现的!具体真正的zval和zend_value的模样,前者是一个struct结构体,后者是一个union联合体!

这个refcount就是传说中的引用计数了,初始化的时候a后面的引用次数为1(注意,正确说法应该是a后面的赋值的数组zend_value引用计数为1,而不是a这个变量zval本身)。然后我们将

math?formula=b%20%3Da,其实相当于又一个变量指向了这个zend_value,所以refcount变为2,最后将

math?formula=c%20%3Da,同理,zend_value的refcount再次加1变成了3。然后,我们用unset(

math?formula=c%20)%EF%BC%8C%E8%BF%99%E4%BC%9A%E5%84%BF%EF%BC%8CC%E8%AF%AD%E8%A8%80%E8%A6%81%E5%81%9A%E7%9A%84%E5%B0%B1%E6%98%AF%E6%8A%8Ac的zval给KO free掉,但是并不是free zend_value,这会儿zend_value的refcount就自然而然减1变成2了。

那么写时拷贝是什么意思呢?看下面代码:

// 先不要问为什么非要加mt_rand,不然,绝笔说不过来了,到处都是坑

$a = 'hello'.mt_rand( 1, 1000 );

$b = $a;

$a = 123;

echo $b.PHP_EOL;

// 运行结果,不用我说吧,脚趾头都知道是'hello'.mt_rand( 1, 1000 )的结果,绝对不可能是123。

其实,当你把

math?formula=a%E8%B5%8B%E5%80%BC%E7%BB%99b的时候,

math?formula=a%E7%9A%84%E5%80%BC%E5%B9%B6%E6%B2%A1%E6%9C%89%E7%9C%9F%E7%9A%84%E5%A4%8D%E5%88%B6%E4%BA%86%E4%B8%80%E4%BB%BD%EF%BC%8C%E8%BF%99%E6%A0%B7%E6%98%AF%E5%AF%B9%E5%86%85%E5%AD%98%E7%9A%84%E6%9E%81%E5%BA%A6%E4%B8%8D%E5%B0%8A%E9%87%8D%EF%BC%8C%E4%B9%9F%E6%98%AF%E5%AF%B9%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%E7%9A%84%E6%9E%81%E5%BA%A6%E4%B8%8D%E5%B0%8A%E9%87%8D%EF%BC%8C%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%BB%85%E4%BB%85%E6%98%AF%E5%B0%86b指向了

math?formula=a%E7%9A%84%E5%80%BC%E8%80%8C%E5%B7%B2%EF%BC%8C%E8%BF%99%E5%B0%B1%E5%8F%AB%E5%A4%9A%E5%BF%AB%E5%A5%BD%E7%9C%81%E3%80%82%E9%82%A3%E4%B9%88%EF%BC%8C%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E7%9C%9F%E6%AD%A3%E7%9A%84%E5%8F%91%E7%94%9F%E5%A4%8D%E5%88%B6%E5%91%A2%EF%BC%9F%E5%B0%B1%E6%98%AF%E5%BD%93%E6%88%91%E4%BB%AC%E4%BF%AE%E6%94%B9a的值为123的时候,这个时候就不得已进行复制,避免

math?formula=b%E7%9A%84%E5%80%BC%E5%92%8Ca的一样。

$a = 'hello'.mt_rand( 1, 1000 );

$b = $a;

echo xdebug_debug_zval('a');

$a = 'world'.mt_rand( 2, 2000 );

echo xdebug_debug_zval('a');

// 运行结果为1,其中的原理你自己应该能理顺了昂

叨逼叨了这么长,通过简单的案例解释清楚了两个要点:引用计数和写时拷贝,那么垃圾回收也该来了。当一个zval在被unset的时候、或者从一个函数中运行完毕出来(就是局部变量)的时候等等很多地方,都会产生zval与zend_value发生断开的行为,这个时候zend引擎需要检测的就是zend_value的refcount是否为0,如果为0,则直接KO free空出内容来。如果zend_value的recount不为0(废话一定是大于0),这个value不能被释放,但是也不代表这个zend_value是清白的,因为此zend_value依然可能是个垃圾。

什么样的情况会导致zend_value的refcount不为0,但是这个zend_value却是个垃圾呢?PHP7中两种情况:

数组:a数组的某个成员使用&引用a自己 (环状引用)

对象:对象的某个成员引用对象自己

$arr = [ 1 ];

$arr[] = &$arr;

unset( $arr );

这种情况下,zend_value不会释放,但也不能放过它,不然一定会产生内存泄漏,所以这会儿zend_value会被扔到一个叫做垃圾回收堆中(垃圾缓存区中),然后zend引擎会依次对垃圾回收堆中的这些zend_value进行二次检测,检测是不是由于上述两种情况造成的refcount为1但是自身确实没有人再用了,如果一旦确定是上述两种情况造成的,那么就会将zend_value彻底抹掉释放内存。

那么垃圾回收发生在什么时候?有些同学可能有疑问,就是php不是运行一次就销毁了吗,我要着gc有何用?并不是啦,首先当一次fpm运行完毕后,最后一定还有gc的,这个销毁就是gc;其次是,内存都是即用即释放的,而不是攒着非得到最后,你想想一个典型的场景,你的控制器里的某个方法里用了一个函数,函数需要一个巨大的数组参数,然后函数还需要修改这个巨大的数组参数,你们应该是函数的运行范围里面修改这个数组,所以此时会发生写时拷贝了,当函数运行完毕后,就得赶紧释放掉这块儿内存以供给其他进程使用,而不是非得等到本地fpm request彻底完成后才销毁。

注重体现重点,很多细节实在没法写,比如我举个例子:

$a=[];

xdebug_debug_zval( $a )的refcount值你猜是多少?

7.1.17下竟然是2,你是不是以为是1,然而并不是。不过你不用纠结这些细节,gc的关键就是能说出引用计数的原理和写时拷贝,很多细节深处都各种奇奇怪怪的东西,面试官自己都不一定知道。

//内存管理机制

var_dump(memory_get_usage());//获取内存方法,加上true返回实际内存,不加则返回表现内存

$a = "laruence";

var_dump(memory_get_usage());

unset($a);

var_dump(memory_get_usage());

//输出(在我的个人电脑上, 可能会因为系统,PHP版本,载入的扩展不同而不同):

//int 240552

//int 240720

//int 240552

定义变量之后,内存增加,清除变量之后,内存恢复(有些可能不会恢复和以前一样),好像定义变量时申请了一次内存,其实不是这样的,php会预先申请一大块内存,不会每次定义变量就申请内存。(避免频繁的向系统申请,浪费时间和资源)

首先我们要打破一个思维: PHP不像C语言那样, 只有你显示的调用内存分配相关API才会有内存的分配. 也就是说, 在PHP中, 有很多我们看不到的内存分配过程.

比如对于:

$a = "laruence";

隐式的内存分配点就有:

1.1. 为变量名分配内存, 存入符号表 zval

2.2. 为变量值分配内存 zend_value

所以, 不能只看表象.

第二, 别怀疑,PHP的unset确实会释放内存, 但这个释放不是C编程意义上的释放, 不是交回给OS.

对于PHP来说, 它自身提供了一套和C语言对内存分配相似的内存管理API:

emalloc(size_t size);

efree(void *ptr);

ecalloc(size_t nmemb, size_t size);

erealloc(void *ptr, size_t size);

estrdup(const char *s);

estrndup(const char *s, unsigned int length);

这些API和C的API一一对应, 在PHP内部都是通过这些API来管理内存的.

当我们调用emalloc申请内存的时候, PHP并不是简单的向OS要内存, 而是会向OS要一个大块的内存, 然后把其中的一块分配给申请者, 这样当再有逻辑来申请内存的时候, 就不再需要向OS申请内存了, 避免了频繁的系统调用.

比如如下的例子:

var_dump(memory_get_usage(true));//注意获取的是real_size

$a = "laruence";

var_dump(memory_get_usage(true));

unset($a);

var_dump(memory_get_usage(true));

//输出

//int 262144

//int 262144

//int 262144

也就是我们在定义变量$a的时候, PHP并没有向系统申请新内存.同样的, 在我们调用efree释放内存的时候, PHP也不会把内存还给OS, 而会把这块内存, 归入自己维护的空闲内存列表. 而对于小块内存来说, 更可能的是, 把它放到内存缓存列表中去(后记, 某些版本的PHP, 比如我验证过的PHP5.2.4, 5.2.6, 5.2.8, 在调用get_memory_usage()的时候, 不会减去内存缓存列表中的可用内存块大小, 导致看起来, unset以后内存不变).

$a = "hello";

//定义变量时,存储两个方面:

//1.变量名,存储在符号表(hashtable 的局部变量)

//2.变量值存储在内存空间 zval结构

//3.在删除变量的时候,会将变量值存储的空间释放,而变量名所在的符号表不会减小(只增不减)

只增不减的数组

Hashtable是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());

我们定义了100个变量, 然后又把他们unset, 来看看输出:

//int 242104

//int 259768

//int 242920(被符号表相应的占去了)

Wow, 怎么增加了这么多内存?

这是因为对于Hashtable来说, 定义它的时候, 不可能一次性分配足够多的内存块,来保存未知个数的元素, 所以PHP会在初始化的时候, 只是分配一小部分内存块给HashTable, 当不够用的时候再RESIZE扩容。而Hashtable, 只能扩容, 不会减少, 对于上面的例子, 当我们存入100个变量的时候, 符号表不够用了, 做了一次扩容, 而当我们依次unset掉这100个变量以后, 变量占用的内存是释放了(118848 – 104448), 但是符号表并没有缩小, 所以这些少的内存是被符号表本身占去了。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值