性能专题 - 内存回收

  • 作者:KK

  • 发表日期:2016.10.14


要点速读

  • 手写unset语句把变量一个个销毁真心累,一般人都不会做,除非必须unset。

  • 函数/方法运行结束后,这个作用域里申请的变量都会被自动销毁,等于自动帮你unset了,所以在作用域运行到结尾时加unset语句绝对是多余的操作!

  • 适当封装函数,这些函数调用完后变量自动销毁,就可以腾出内存给下一个函数利用这些内存,内存峰值增长就可以放缓。

  • 多封装代码,上百行处理的代码必然导致内存峰值持续增长,这种代码必须封装优化才能在大并发情况下有更好的性能表现。


正文

明白了内存峰值后,我想很多程序员会想着:哎呀原来unset后,新变量会利用旧变量的内存空间呀,这样就可以缓解内存峰值上升的速度了,那我得积极地去unset呀!

然而你看过有哪个开源程序真的去积极地unset变量的呢?————答案是因为根本没必要这样做。


手动写销毁的变量既繁杂又让人感到心累

随便打开几份你手上现有的代码,观察下里面的变量,在这个变量已经不用的位置开始,你为它添加unset处理,再找找其它变量继续写unset语句……

估计你写到第三个变量就会开始心累了,这有完没完啊,是不是每个变量用完都要你去unset呢?

但至少这个内心感受已经证明了手动写unset代码去销毁每一个变量是不靠谱的!————PHP可以为你自动unset。


函数结束后会自动销毁变量

function test(){
	$a = array_fill(0, 1000, time());
}

echo memory_get_peak_usage() . PHP_EOL; //123440 注意下一次的峰值

test();
echo memory_get_peak_usage() . PHP_EOL; //172056 test函数增加了大数组,增值较上次激增

test();
echo memory_get_peak_usage() . PHP_EOL; //172152 峰值增长变得很微小了

第二次增长微秒,可以判断这些增长多数是由于函数调用的运算成本用的,然而再次进入test函数里再申请变量时没有发生二次激增。

原因就是第一次test函数调用结束后,test里面的变量a已经被自动销毁了,PHP把这些内存暂时保管起来,下次再有需要新增变量,就优先从使用这些腾出来的变量空间。

所以上面用test函数的代码将比以下这些不用test函数的代码内存峰值更低:

echo memory_get_peak_usage() . PHP_EOL; //120272

$a = array_fill(0, 1000, time());
echo memory_get_peak_usage() . PHP_EOL; //171152 突然产生个大数组,峰值必然激增

$b = array_fill(0, 1000, time());
echo memory_get_peak_usage() . PHP_EOL; //223416 由于没有删除变量腾出空间,二次产生大数组,于是又一次峰值激增

小结

通常情况下适当封装函数调用,反而能节省内存,因为函数调用完就会释放变量空间留给其他新增变量使用

这一点我认为非常重要,通常很多才做两三年的普通程序员都觉得老是封装,调来调去好麻烦。

其实纵观各大开源框架的代码,都是一层层互相调用,一层接一层,每层只做自己的事,做完就返回,释放空间,把这些空间留给其他环节使用,这样既令程序结构清晰而又节省了内存。

但是其实并不是所有封装都必然能节省内存的,要看具体情况,你看看代码的主要逻辑,特大数组/对象的处理加以思考就可以判断出你目前的情况使用封装会不会有利于内存使用。

然而很经常见到一些代码是在一个控制器的方法里写几百行代码肯定是浪费内存的,不如将主要的处理环节封装成各个函数,让一个环节工作完就能空出一些内存不更好?否则这必然会导致内存峰值持续增长,这种代码必须优化才能在大并发情况下有更好的性能表现。

具体哪些代码要封装哪些不要封装难以一概而论,需要看你的情况,一般情况下都是按照代码责任范围来划分一个个类和方法。