在回调和闭包中的内存泄漏
近来老见到人有内存泄漏的问题,自己写模块和例子的时候,也发现有内存泄漏的问题。。。学艺不精啊,所以特在这写一个文章来分享一下有关这方面的内容。
因为回调和闭包在事件程序中最多,所以我很早以前就找过一个有关这个的文章 <<AnyEvent and memory leaks >> 这个文章的作者,见到了 kraih (Mojolicious 的作者) 放了一个 gist link 上面一个简单的内存泄漏的例子。这指出了,对于初学者来讲,写这种事件回调程序时最容易出的错的地方, 所以对于我们写 AnyEvent (Mojolicious 之类的应用)之类的程序员,了解这块非常非常有用,这样我们对可能造成的内存泄漏之类问题就知道怎么应对和处理 .
闭包的内存泄漏实例
kraih 的例子如下:
# Very common leak my$foo; $foo= sub{ my($i, $j) = @_; returnif$i>= $j; say $i++; $foo->($i, $j); }; $foo->(1, 10);
测试是否内存泄漏的方式
对于上面这个例子,是一定有内存泄漏的,这时我们要怎么样来测试内存泄漏啦?
方法 1: 使用 Devel::Cycle 模块,来测试引用, 如上面的例子,我们可以在最后面加上
find_cycle $foo;
这样可以见到如下输出:
Cycle (1): $Avariable $foo=> \$B $$B=> \&A
上面可以直接见到变量一直存在对自身的引用,内存所以不被释放。
方法2:上面的方式太复杂是吧。。。因为有时是一个对象,有时你根本不知道什么地方出错,这时我们有个法子进行压力测试,这样来让问题暴露出来,我们还是拿上面的例子来看。
$0= 'perl_MEM_TEST'; while(1) { { my$foo; $foo= sub{ my($i, $j) = @_; returnif$i>= $j; #say $i++; $foo->($i, $j); }; $foo->(1, 10); } }
嗯,我们运维的思想, 这时只要 ps 来检查内存占用就好。因为 while 所以每秒的次数会非常非常高。一会问题就出来了。
# ps -eo pid,user,comm,args,%cpu,rss,vsz|grep TEST|grep -v grep 10366 root perl perl_MEM_TEST 101 2094848 2310136
我们可以见到 rss,基本上几秒以后就快速的增长,然后接下来就简单了,一个个的注掉函数,不断的隔离函数,直到定位到出了问题的函数。
内存泄漏的原因
kraih 例子中的 bug 是因为 $foo 声明是在闭包之外而引起的. 但在函数内部的代码访问了这个闭包的变量。 这时变量的引用计数器增加 了。基本上所有的内存泄漏都是这个原因。从上面 find_cycle 都是这样。
在看一个例子,前几天我所犯下的错误,经 py 指正过的。我在 AnyEvent 的应用中的例子,我想使用 begin end 来做回调组的同步,例子的例如下:
while(1) { my$cv= AE::cv; $cv->begin(sub{ $cv->send; }); my$w= AE::timer 0, 0, sub{ $cv->end; }; $cv->recv; }
也可以使用上面的方法 2 来进行内存泄漏的测试,也能见到内存快速的增长,但大家见到上面的错误了吗? 嗯,这个错误很深,只是一个简单的错误。因为我在第一个 begin 中使用了 $cv ,但这个 $cv 是直接引用的外面的变量,所以让引用计数器在不断的增加。
其实作者也做了一样的的处理,这个时候,其实我们一定要使用传进来的 $cv 不要使用外部的 cv ,所以这个地方 $cv->send 必须要写成 shift->send 或者 $_[0]->send; 所以于对这种闭包的使用,我们最好都使用传参数的方式给所需要的函数引用传进去 。
纠正内存泄漏
对于 kraih 的例子中,要纠正这个函数,方式是象下面一样,给要调用的函数本身通过传参的方式来传进去,这是 <<AnyEvent and memory leaks>> 中提供的方式.
my$foo; $foo= sub{ my($foofoo, $i, $j) = @_; returnif$i>= $j; say $i++; $foofoo->($foofoo, $i, $j); undef$foofoo; #this line is optional }; $foo->($foo, 1, 10);
我们使用传参的方式,给函数传进去后就一切正常了。但上面的方法并不是很好看和好懂,这种闭包多了,函数的层级也会非常多,比如 AnyEvent::HTTP 模块中,就大量使用了这种闭包。
下面提供一种我认为很好的方式,其实我其它的文章中也提到过.
{ packageT; useMoo; subfoo { my$self= shift; returnsub{ my($i, $j) = @_; returnif$i>= $j; say $i++; $self->foo->($i, $j); } }; 1; } my$t= T->new; $t->foo->(1, 10);
给需要的东西,包成模块,然后给闭包这种难懂的东西,封装起来。当然可能不一定要使用闭包,只是自己调用自己的话,就可以不用这样,上面对于于象 AnyEvent::HTTP 中的回调 on_body 之类这种需要一个代码引用的时候,非常有用,因为 $self->foo 是返回一个代码引用。可以直接
on_body => $self->foo;
这样很方便的使用,在需要大量回调的程序中,还不用给闭包堆得超级超级多,一层又一层。
建议继续学习:
- for 循环为何可恨? (阅读:4507)
- iOS内存暴增问题追查与使用陷阱 (阅读:4351)
- 理解Javascript的闭包 (阅读:3837)
- GC与JS内存泄露 (阅读:3530)
- JavaScript的闭包问题 (阅读:3369)
- 闭包漫谈(从抽象代数及函数式编程角度) (阅读:3187)
- 为什么重复free()比内存泄漏危害更大 (阅读:3044)
- 什么是闭包(Closure)? (阅读:2915)
- 前端代码之丑(一):分支化技巧 (阅读:2822)
- 闭包与作用域 (阅读:2884)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:扶 凯 来源: 扶凯
- 标签: 内存泄漏 回调 闭包
- 发布时间:2015-02-03 22:13:35
- [68] Twitter/微博客的学习摘要
- [66] IOS安全–浅谈关于IOS加固的几种方法
- [64] android 开发入门
- [64] 如何拿下简短的域名
- [62] find命令的一点注意事项
- [61] Go Reflect 性能
- [60] 流程管理与用户研究
- [59] Oracle MTS模式下 进程地址与会话信
- [58] 图书馆的世界纪录
- [56] 读书笔记-壹百度:百度十年千倍的29条法则