JavaScript中的内存泄漏和闭包 - 何时以及为什么会发生?

68

你经常在网上看到使用闭包是JavaScript中内存泄漏的一个巨大来源。大多数情况下,这些文章指的是混合脚本代码和DOM事件,其中脚本指向DOM,反之亦然。

我知道,在那种情况下,闭包可能会成为问题。

但是Node.js呢?在这里,我们自然没有DOM - 所以不存在像浏览器中那样有内存泄漏的副作用。

闭包还可能存在哪些问题?有人能详细说明或指点我一个好的教程吗?

请注意,此问题明确针对Node.js,而不是浏览器。


我不确定我理解你所寻找的内容。你需要担心对象如何被管理,闭包可能会使持有对象变得不那么明显,但是...代码可能会创建太多对象并耗尽内存。 - WiredPrairie
当然可以。但是有没有最佳实践,或者有没有需要注意的模式以避免内存泄漏?由于Node.js的异步特性,我发现很难想象不使用闭包。因此,如果我必须使用它们,我应该遵循什么准则?换句话说,我可能会遇到哪些问题? - Golo Roden
3
我能提供的最佳建议是:“理解闭包”。一旦你理解了闭包,就能消除它们的神秘感,而且你不需要特别的指导来使用它们。 - WiredPrairie
2
我理解什么是闭包,但我缺乏对V8内存管理的知识,例如...因此我正在寻求具体建议;-) - Golo Roden
3个回答

47

这个问题类似。基本上,如果在回调中使用闭包,当你完成后应该“取消订阅”回调,这样GC就知道不能再次调用它。我觉得这很有道理;如果你有一个闭包在等待被调用,GC将难以知道你已经完成了它。通过手动从回调机制中删除闭包,它就变得未引用且可供收集。

此外,Mozilla发表了一篇关于发现Node.js代码中内存泄漏的好文章。我认为,如果你尝试他们的一些策略,你可以找出表现出漏气行为的代码部分。最佳实践很好,但我认为更有帮助的是了解程序的需求,并根据你可以经验性地观察到的情况提出一些个性化的最佳实践。

以下是来自Mozilla文章的快速摘录:

  • Jimb Esser的node-mtrace,使用GCC的mtrace工具对堆使用情况进行分析。
  • Dave Pacheco的node-heap-dump对V8堆进行快照,并将整个内容序列化到一个巨大的JSON文件中。它包括用于在JavaScript中遍历和调查结果快照的工具。
  • Danny Coates的v8-profilernode-inspector为V8分析器提供了Node绑定和使用WebKit Web Inspector的Node调试接口。
  • Felix Gnass的同样的分叉,取消了保留者图形的禁用。
  • Felix Geisendörfer的《Node Memory Leak Tutorial》是一个简短而精炼的解释如何使用v8-profilernode-debugger,并且现在对于大多数Node.js内存泄漏调试来说是最先进的。
  • Joyent的SmartOS平台,为调试Node.js内存泄漏提供了一系列有用的工具。

这个问题的答案基本上都说你可以通过将闭包变量赋值为null来帮助GC。

var closureVar = {};
doWork(function callback() {
  var data = closureVar.usefulData;
  // Do a bunch of work
  closureVar = null;
});

在函数内声明的任何变量都会在函数返回时消失,除非它们被用于其他闭包中。在这个例子中,closureVar 必须一直保留在内存中直到调用 callback() 函数,但是谁知道那会发生在什么时候呢?一旦回调被调用,你可以通过将闭包变量设置为 null 来向 GC 提供提示。

免责声明:正如您可以从下面的评论中看到的那样,有些 SO 用户说这些信息已经过时,在 Node.js 中没有实际意义。我还没有一个明确的答案; 我只是发布了我在网上找到的内容。


我同意plalx的观点,设置null不会有任何作用。这是在旧版IE浏览器中必须使用的技巧,因为垃圾回收机制不好。几乎没有证据表明它会对V8或Mozilla Seamonkey造成问题。你链接的那个问题已经两年了,我怀疑它在这里是否相关。在同一个问题中,delnan的评论说循环闭包对V8不是问题。 - user568109
1
我也读了这个问题,并考虑回答闭包在集合中不安全的问题。但是在网上验证后,我发现所有这些文章都与DOM有关,暗示着浏览器相关的GC问题(IE?)。而且这些文章都是几年前的。我找不到任何证据表明V8 GC未能收集闭包模式。此外,没有基准来证明这种技术适用于V8。 - user568109
我还没有时间在“将变量设置为null”的策略上运行测试代码,因此我添加了一份免责声明。你们两个中的任何一个(@plalx或@user568109)有否提供某个基准测试结果的参考,证明了那个 Stack Overflow 的回答是错误的? - RustyTheBoyRobot
1
@user568109,我必须指出,在事件监听器的上下文中,plax的评论是有效的。但是在这里说“设置null什么也不做”可能会让一些读者认为这种技术在一般情况下没有帮助,这是错误的。真正重要的是变量指向已分配的内存。如果它是可达的,它将不会被释放。当您将变量设置为null时,分配的内存将不可达并将被垃圾回收。 - borisdiakur
@Lego,你的评论完全正确,请注意这个问题被标记为node.js,并且OP明确要求使用node.js。我不是在谈论一般情况。OP问的是它与通常的浏览器场景有何不同。此外,我无法强调不要发布旧博客和传闻的重要性。几乎没有任何真实证据表明它适用于node。它可能曾经是有效的,但现在已经不是了。 - user568109
@user568109 有参考资料吗?我已经安装了node v0.10.24,但仍然遇到并解决了使用上述技术时的内存泄漏问题。 - borisdiakur

14

你可以在David Glasser的博客文章中找到一个好的例子和解释。

好的,这就是它(我添加了一些注释):

var theThing = null;
var cnt = 0; // helps us to differentiate the leaked objects in the debugger
var replaceThing = function () {
    var originalThing = theThing;
    var unused = function () {
        if (originalThing) // originalThing is used in the closure and hence ends up in the lexical environment shared by all closures in that scope
            console.log("hi");
    };
    // originalThing = null; // <- nulling originalThing here tells V8 gc to collect it 
    theThing = {
        longStr: (++cnt) + '_' + (new Array(1000000).join('*')),
        someMethod: function () { // if not nulled, original thing is now attached to someMethod -> <function scope> -> Closure
            console.log(someMessage);
        }
    };
};
setInterval(replaceThing, 1000);

请在Chrome Dev Tools(时间轴标签,内存视图,单击记录)中尝试使用和不使用originalThing进行空值处理。请注意,上面的示例适用于浏览器和Node.js环境。

特别鸣谢Vyacheslav Egorov


你能添加其他提到的例子吗?还要说明这段代码片段预计会泄漏,可以通过额外的行将其设置为null来修复。 - user568109
@borisdiakur 感谢您提供的代码片段。在经过漫长的审查之后,这是第一个真正有用的帖子!但请让我再问一个问题...如何处理这种行为才是合适的?在任何不共享的回调中将每个变量设置为 null 是否是合适的方式?实际上,我使用 --expose-gc 运行节点,并定期调用 global.gc(),与没有该选项的第二个实例相比,它可以保持内存稳定(100Mb vs. 不断增长的 300Mb)。 - Bernhard
@Bernhard 使用 --expose-gc 并定期调用 gc() 是不好的实践。V8 通过增量标记和惰性清除根据需要收集垃圾。处理上述行为的适当方法是将指向已成为词法环境一部分的对象的本地变量置空。 - borisdiakur
@borisdiakur 谢谢,没错,我也找到了类似的信息。但是node在内部如何处理内存呢?因为即使很长一段时间后,当我通过htop查看时,内存并没有减少。或者是应用程序只是保留了内存而没有将空闲内存返回给操作系统->因为堆转储大小仅为“使用”内存的25%。 - Bernhard
1
@Bernhard 我建议阅读《V8垃圾回收之旅》(http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection)。我的经验是,当内存不足时,V8才会释放内存。例如,如果您使用“--max-old-space-size=3072”运行应用程序,并且未使用超过3GB,则根本不会释放内存。 - borisdiakur
显示剩余2条评论

2
我不同意闭包是内存泄漏的原因。这可能对于旧版本的IE来说是真的,因为它的垃圾回收机制很糟糕。请阅读Douglas Crockford的这篇文章,其中清楚地说明了什么是内存泄漏。
"未被回收的内存被称为泄漏。"
泄漏并不是问题,高效的垃圾回收才是关键。泄漏可能会在浏览器和服务器JavaScript应用程序中发生。以V8为例,在浏览器中,当您切换到不同的窗口/选项卡时,将进行垃圾回收。空闲时会修补泄漏。选项卡可以处于空闲状态。
但在服务器上情况就没有那么容易了。泄漏可能会发生,但GC的成本效益并不高。服务器不能经常进行GC,否则其性能将受到影响。当节点进程达到一定的内存使用量时,它会启动GC。然后定期删除泄漏。但泄漏仍然可能以更快的速度发生,导致程序崩溃。

欢迎任何建设性的批评。请给出评论的人加上评论。 - user568109
1
我猜有些读者可能会把你的回答理解为“闭包不可能是内存泄漏的原因”。内存泄漏在闭包中很容易被隐藏起来:https://github.com/jedp/node-memwatch-demo#some-examples-of-leaks - borisdiakur
3
请查看此篇博客文章中列出的第三个例子,并在Chrome开发者工具中尝试它(时间轴选项卡,记忆视图,点击记录)。您可以随意点击垃圾桶图标,v8垃圾回收器不会回收已分配的内存。对我来说,v8垃圾回收器似乎并不像你所说的那么聪明。如果你能证明我错了(我不喜欢给人差评,这会让我失去一个互联网积分,而且我总是在给人差评之前阅读完整个答案),我将很高兴能更好地理解这一点,并撤销我的差评。:] - borisdiakur
1
@borisdiakur 这个例子涉及到全局变量,这也是内存泄漏的根本原因 - 全局变量。闭包与泄漏无关。但是,它们确实可以隐藏泄漏。 - setec
1
这似乎变成了一个语义争论,但我认为最重要的是要知道闭包可以轻松地加剧由全局变量、未清理的事件侦听器等最初引起的内存泄漏。无论闭包是否是泄漏的根本原因,它所内部引用的任何内存都会被泄漏。 - Andy
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接