常见的 Perl 内存/引用泄漏模式?

Posted

技术标签:

【中文标题】常见的 Perl 内存/引用泄漏模式?【英文标题】:Common Perl memory/reference leak patterns? 【发布时间】:2010-02-08 18:01:11 【问题描述】:

我正在研究 Perl 代码库中的几个潜在内存泄漏,我想了解有关 Perl 中的内存(错误)管理的常见缺陷。

您在 Perl 代码中观察到哪些常见的泄漏模式?

【问题讨论】:

您是如何发现这些内存泄漏的?您是否尝试过 Perl Profiler (perlmonks.org/?node_id=472366)? 【参考方案1】:

循环引用是迄今为止最常见的泄漏的典型原因。

sub leak 
    my ($foo, $bar);
    $foo = \$bar;
    $bar = \$foo;

Perl 使用引用计数垃圾收集。这意味着 perl 会记录在给定时间存在的指向任何变量的指针。如果变量超出范围且计数为 0,则变量被清除。

在上面的示例代码中,$foo$bar 永远不会被收集,并且在每次调用 leak() 后都会保留一份副本,因为这两个变量的引用计数均为 1。

防止此问题的最简单方法是使用弱引用。弱引用是您在访问数据时遵循的引用,但不计入垃圾回收。

use Scalar::Util qw(weaken);

sub dont_leak 
    my ($foo, $bar);
    $foo = \$bar;
    $bar = \$foo;
    weaken $bar;

dont_leak()中,$foo的引用计数为0,$bar的引用计数为1。当我们离开子程序的作用域时,$foo被返回到池中,它的引用$bar 被清除。这会将$bar 上的引用计数降至 0,这意味着 $bar 也可以返回池中。

更新: Brain d foy 询问我是否有任何数据来支持我的断言,即循环引用很常见。不,我没有任何统计数据表明循环引用很常见。它们是 perl 内存泄漏最常被谈论和记录最完整的形式。

我的经验是它们确实会发生。以下是我在使用 Perl 十多年来看到的内存泄漏的简要概述。

我遇到了 pTk 应用程序泄漏的问题。我能够证明的一些泄漏是由于 Tk 传递窗口引用时出现的循环引用。我还看到了 pTk 泄漏,其原因我永远无法追查。

我看到人们误解了 weaken 并意外地得到了循环引用。

当太多考虑不周的物体被匆忙拼凑在一起时,我看到了无意的循环。

有一次,我发现一个 XS 模块产生了内存泄漏,该模块正在创建大而深的数据结构。我永远无法获得比整个程序更小的可重现测试用例。但是当我用另一个序列化程序替换模块时,泄漏就消失了。所以我知道这些泄漏来自 XS。

因此,根据我的经验,周期是泄漏的主要来源。

幸运的是,there is a module 帮助追踪他们。

关于永远不会清理的大型全球结构是否构成“泄漏”,我同意布赖恩的观点。它们像泄漏一样嘎嘎作响(由于错误,我们的进程内存使用量不断增长),因此它们是泄漏。即便如此,我不记得曾经在野外看到过这个特殊的问题。

根据我在巨石阵网站上看到的情况,我猜布赖恩从他正在训练或为治愈奇迹的人那里看到了很多病态的代码。所以他的样本集很容易比我的更大、更多样化,但它有自己的选择偏差。

哪种泄漏原因最常见?我想我们永远不会真正知道。但我们都同意循环引用和全局数据垃圾场是反模式,需要尽可能消除它们,并在少数有意义的情况下谨慎处理。

【讨论】:

有什么支持循环引用作为最常见的形式吗?我几乎看不到这个问题,但我确实看到有人使用他们从未清除过的全局哈希。 @brian d foy:严格来说,这不是内存泄漏,只是内存使用过多。在程序无法释放内存之前,这不是内存泄漏 大多数人并不严格关心为什么他们的记忆不断增长缓慢(或不那么缓慢)。 :) +1。当面临泄漏时,一个简单的第一步是在一个单元测试中添加Test::Memory::Cycle 测试everywhere 我已经使用 Perl 6 年了,我遇到的第一次内存泄漏是一个经常调用的子例程中局部变量的循环引用问题(在父子程序之间具有双向引用的树)。我什至不知道 Perl 不会自动处理“无法访问的数据对象”。【参考方案2】:

如果问题出在 Perl 代码中,您可能有一个指向自身或父节点的引用。

通常它以引用父对象的对象的形式出现。

 package parent;
  sub new bless  'name' => $_[1] , $_[0] 
  sub add_child
    my($self,$child_name) = @_;
    my $child = child->new($child_name,$self);
    $self->$child_name = $child;   # saves a reference to the child
    return $child;
  

 package child;
  sub new
    my($class,$name,$parent) = @_;
    my $self = bless 
      'name' => $name,
      'parent' => $parent # saves a reference to the parent
    , $class;
    return $self;
  


  my $parent = parent->new('Dad');
  my $child  = parent->add_child('Son');

  # At this point both of these are true
  # $parent->Sonparent == $parent
  # $child->parentSon  == $child

  # Both of the objects **would** be destroyed upon leaving
  # the current scope, except that the object is self-referential


# Both objects still exist here, but there is no way to access either of them.

解决此问题的最佳方法是使用Scalar::Util::weaken。

use Scalar::Util qw'weaken';
 package child;
  sub new
    my($class,$name,$parent) = @_;
    my $self = bless 
      'name' => $name,
      'parent' => $parent
    , $class;

    weaken $$self->parent;

    return $self;
  

如果可能的话,我建议从子对象中删除对父对象的引用。

【讨论】:

值得一提的是,对于对象,您还可以使用析构函数来破坏任何循环引用。您的 Child 类可以有 sub DESTROY $_[0]->parent = undef; ,甚至是 sub DESTROY $_[0] = undef; ,并且不需要弱引用。弱引用是处理事情的更好方法,它们使正确的行为自动发生。还值得注意的是,Moose 具有自动弱化属性引用的功能:search.cpan.org/dist/Moose/lib/Moose/Manual/… 这是 Moose 使 OO Perl 更好的另一种方式。 @daotoad:您不能使用析构函数来破坏循环引用,因为在引用计数降至零之前不会调用析构函数。如果调用了析构函数,则不会出现循环引用问题。如果你有循环引用,你要么需要使用弱引用,要么在完成对象后手动调用析构函数(这很容易出错)。 @cjm,你是对的。有一阵子了。 weaken 真的是要走的路。您必须创建一个容器对象来保存循环引用并让容器的析构函数打破循环。另一种选择是有一个明确的方法调用来清除一个小部件并打破任何循环。比如 Perl Tk 的Widget::destroy() 方法。【参考方案3】:

过去我在使用 XS 时遇到过问题,无论是我自己手工制作的东西还是 CPAN 模块,如果管理不当,C 代码中的内存就会泄漏。我从来没有设法追踪泄漏。该项目的截止日期很紧,并且有固定的运行生命周期,所以我通过每天cron 重新启动来解决这个问题。 cron 真是太棒了。

【讨论】:

【参考方案4】:

CPAN 中的一些模块使用循环引用来完成它们的工作,例如html::TreeBuilder(代表 HTML 树)。他们将要求您在最后运行一些破坏性方法/例程。只需阅读文档 :)

【讨论】:

以上是关于常见的 Perl 内存/引用泄漏模式?的主要内容,如果未能解决你的问题,请参考以下文章

如何在长时间运行的 Perl 程序中找到内存泄漏?

发现 Perl 内存泄漏

使用单例模式造成的内存泄漏

常见的内存泄漏原因及解决方法

内存泄漏MVP模式中的内存泄漏以及解决方案

JS中4种常见的内存泄漏