IT技术博客大学习 共学习 共进步

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

扶凯 2015-02-03 22:13:35 浏览 3,982 次

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

因为回调和闭包在事件程序中最多,所以我很早以前就找过一个有关这个的文章 <<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. iOS内存暴增问题追查与使用陷阱 (阅读 5,682)
  2. for 循环为何可恨? (阅读 5,381)
  3. 理解Javascript的闭包 (阅读 4,741)
  4. GC与JS内存泄露 (阅读 4,600)
  5. JavaScript的闭包问题 (阅读 4,301)
  6. 为什么重复free()比内存泄漏危害更大 (阅读 3,920)
  7. 闭包漫谈(从抽象代数及函数式编程角度) (阅读 3,780)
  8. 什么是闭包(Closure)? (阅读 3,761)
  9. 回调还是消息队列 (阅读 3,763)
  10. 闭包与作用域 (阅读 3,721)