技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 算法 --> 在回调和闭包中的内存泄漏

在回调和闭包中的内存泄漏

浏览:3100次  出处信息

近来老见到人有内存泄漏的问题,自己写模块和例子的时候,也发现有内存泄漏的问题。。。学艺不精啊,所以特在这写一个文章来分享一下有关这方面的内容。

因为回调和闭包在事件程序中最多,所以我很早以前就找过一个有关这个的文章 <<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;

这样很方便的使用,在需要大量回调的程序中,还不用给闭包堆得超级超级多,一层又一层。

建议继续学习:

  1. for 循环为何可恨?    (阅读:4507)
  2. iOS内存暴增问题追查与使用陷阱    (阅读:4351)
  3. 理解Javascript的闭包    (阅读:3837)
  4. GC与JS内存泄露    (阅读:3530)
  5. JavaScript的闭包问题    (阅读:3369)
  6. 闭包漫谈(从抽象代数及函数式编程角度)    (阅读:3187)
  7. 为什么重复free()比内存泄漏危害更大    (阅读:3044)
  8. 什么是闭包(Closure)?    (阅读:2915)
  9. 前端代码之丑(一):分支化技巧    (阅读:2822)
  10. 闭包与作用域    (阅读:2884)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1