JavaScript闭包中的内存泄漏风险

29

已解决

关于这个问题,网络上有很多矛盾的信息。感谢 @John,我设法找出以下闭包不是内存泄漏原因的事实,并且即使在IE8中,它们也不像人们声称的那样普遍。实际上,在我的代码中只发生了1次泄漏,而这并不难修复。

从现在开始,我对这个问题的回答将是:
据我所知,只有当事件附加/处理程序设置在全局对象上时(window.onloadwindow.onbeforeunload等),IE8才会出现泄漏。要解决此问题,请参见下面的答案。


重大更新:

我完全迷失了...经过一些时间查找旧和新的文章和教程,我留下了至少一个巨大的矛盾。一位JavaScript大师(Douglas Crockford)说:

由于IE无法完成其工作并回收周期,因此我们需要这样做。如果我们明确地打破这些周期,那么IE就能够回收内存。根据微软公司的说法,闭包是内存泄漏的原因。当然,这是非常错误的,但它导致微软向程序员提供非常糟糕的建议以应对微软的错误。事实证明,在DOM方面很容易打破这些周期。在JScript方面几乎不可能打破它们。

正如 @freakish 指出我的代码片段与jQuery的内部工作方式类似,我对我的解决方案不会引起内存泄漏感到相当安全。同时,我还发现了此MSDN页面,其中使用闭包的循环引用部分特别吸引了我。下面的图形表示了我的代码的原理,不是吗:

使用闭包的循环引用

唯一的区别在于我有常识,不会将事件侦听器直接附加到元素本身。
尽管如此,Douggie非常明确:IE中闭包不是内存泄漏的来源。这个矛盾使我对谁是正确的毫无头绪。

我还发现在IE9中,泄漏问题也没有完全解决(目前找不到链接)。

最后一件事:我还了解到,IE在JScript引擎之外管理DOM,当我基于ajax请求更改


我不知道为什么 xhr.onreadystatechange=callback 对你不起作用。有趣。但实际上,这部分根本不重要,内存泄漏才是最有趣的事情。虽然我有一段时间没有使用 IE<9,但我从未见过这样的行为。 - freakish
2
顺便说一句:您不必使用jQuery,但可以查看其源代码。 :) 我已经这样做了,似乎他们并没有做任何与您不同的事情。它不应该导致内存泄漏。您确定您没有在某个地方保留对xhrprepareAjax结果的引用吗? - freakish
我经常看jQuery是如何做事的。不,我不使用任何全局变量,因此在成功回调返回后不应该有对xhr的引用。在这种情况下,我可以假设不会有任何内存泄漏。谢谢!将其作为答案发布,我会尽快接受它。 - Elias Van Ootegem
我也不知道。我想知道使用闭包传递引用到DOM元素是否存在内存问题的可能性。我记不得在哪里或何时,但你知道当你有一种直觉或模糊的回忆,认为某个代码可能会出问题吗?这就是我发布这个问题的原因。关于回调:xhr.onreadystatechange = callback在IE中可以工作,但只能工作一次,更离奇的是。我只能假设该函数绑定到了本地的xhr,需要一个本地函数。你回答了标题问题,所以我认为这个问题已经解决了。我对IE有一些想法,但字符数为0。 - Elias Van Ootegem
1
嗯,阅读了您给我们的链接后,我认为您也可以尝试“重新绑定”处理程序,即解除绑定并重新绑定(您可以在body上使用它),并在onUnload事件上放置解绑机制。至于IE的元素删除机制,我只想说一句话:天哪! :) - freakish
显示剩余4条评论
2个回答

25

我曾与微软JavaScript项目的前程序经理合作,开展过一个非浏览器EcmaScript(哦.. JScr... JavaScript)项目,并就闭包进行了一些长时间的讨论。最终,问题在于它们难以GC(垃圾回收),而不是不可能。我得读一下DC对微软“错误”的闭包导致内存泄漏的讨论 - 因为在IE旧版实现中,闭包确实存在问题,因为它们很难通过GC去处理使用微软实现。 我觉得奇怪的是,雅虎的人会试图告诉微软架构师他们代码中已知的问题其实是出在其他地方。尽管我欣赏他的工作,但我不明白他有什么依据。

请记住,你参考的文章是指IE6,因为在写作时,IE7仍在紧锣密鼓地开发中。

顺便说一句-感谢上帝,IE6已经死了(别再让我挖出葬礼照片了)。 虽然,请不要忘记它的遗产...我还没见过有人能够可信地争辩说它在发布的第一天不是世界上最好的浏览器 - 问题在于他们赢得了浏览器大战。 因此,在他们历史上的一个更大的错误之一中-他们在此之后解雇了那个团队,并让它停滞了近5年。 IE团队只有4或5个人处理漏洞修复多年,情况变得非常糟糕。 当他们重新聘用团队并意识到他们所处的位置时,由于处理已经无人理解的单调代码库的附加负担,他们落后了几年。 那是我作为公司内部人士的看法,但没有直接与该团队联系。

也请记住,IE从未为闭包进行过优化,因为当时还没有PrototypeJS(甚至没有Rails),而jQuery也几乎只是Resig脑海中的想法。

在写作时,他们还针对拥有256兆内存的机器进行优化,这些机器也没有被淘汰。

在你让我阅读完整本书的问题之后,我认为公平的是,给你上一节历史课。

总之,我的观点是你在引用极为过时的资料。是的,在IE6中避免使用闭包,因为它们会导致内存泄漏——但那时候IE6是什么没有问题呢?

总之,这是微软已经解决并继续解决的问题。你将要创建某个级别的闭包,即使在当时也是如此。

我知道他们在IE8周围在这个领域做了很多工作(因为我的不可命名的项目使用了非标准JavaScript引擎),并且这项工作一直持续到IE9 / 10。 StatCounter(http://gs.statcounter.com/)表明,IE7的市场份额已降至1.5%,去年从6%下降,并且在开发“新”站点时,IE7变得越来越不相关。您还可以为NetScape 2.0开发,该版本引入了JavaScript支持,但这只会略微愚蠢而已。

真的……不要试图为不存在的引擎过度优化。


1
在阅读你的回答时,我忍不住笑了好几次。我没有过度优化旧引擎的意图。确实,DC链接已经非常过时了。但是老实说,内存泄漏经常被谈论,但很难确定是什么原因导致了它们,为什么以及在哪里。我提供的MSDN链接讨论了DOM对象的循环引用,这非常接近我正在做的事情,再加上对DOM元素管理没有真正的控制,我变得好奇/紧张。我正在寻找一个“明确”的答案:“是什么原因导致了内存问题,哪个浏览器,为什么以及如何避免它们?” - Elias Van Ootegem
简短的回答是Flash。更长的回答是你说得对...你无法访问垃圾收集。内存管理实际上取决于浏览器供应商,并且除非您可以访问(并愿意跟踪)源代码,否则它将真正成为一个封闭的盒子,因为单个浏览器实际上不会给您此访问权限。MSDN文章中的循环引用反映了IE处理对象的早期实现(我说IE,因为它是DOM+script的组合),我怀疑它今天是否仍然正确。 - John Green
1
任何明确的答案都会在开发人员意识到问题并花时间修复它之后过时。此外,新功能将添加新的泄漏(Canvas)。肯定有一些东西会始终导致内存泄漏(例如,在全局范围JS中持久化事物或将大量渲染对象添加到DOM中)。话虽如此,恐怕这个问题太模糊了,没有明确的答案。 - John Green
1
顺便提一下,如果你错过了——我的Flash参考是如何创建内存泄漏的一个例子。;) - John Green
这又是一个太宽泛的问题了...你不能用C#做到,当然也不能用C++做到,你怎么能认为你可以用JS做到呢?: ) 特别是每个版本和小版本都会改变(有时会发生巨大变化)问题集。大多数浏览器都会为您提供特殊工具来检查事物。实际上,我刚在IE中找到了这个:http://blogs.msdn.com/b/gpde/archive/2009/08/03/javascript-memory-leak-detector-v2.aspx 请注意,它建议循环对象问题已完全得到缓解。所以...你引用的所有旧信息都已过时。 - John Green
显示剩余4条评论

4

经过使用 IEJSLeaksDetector 工具一段时间后,我发现我在原问题中提到的事情不会引起内存泄漏。然而,确实出现了一个泄漏。幸运的是,我成功地找到了解决方案:

我有一个主脚本,在底部有一个老式的:

window.onload = function()
{
    //do some stuff, get URI, and call:
    this['_init' + uri[0].ucFirst()](uri);//calls func like _initController
    //ucFirst is an augmentation of the String.prototype
}

这会导致IE8中的泄漏问题,我无法通过window.onbeforeunload处理程序进行修复。似乎您需要避免将处理程序绑定到全局对象。解决方案在于使用闭包和事件监听器,虽然有些麻烦,但以下是我最终采取的方法:

(function(go)
{//use closure to avoid leaking issue in IE
    function loader()
    {
        var uri = location.href.split(location.host)[1].split('/');
        //do stuff
        if (this['_init' + uri[0].ucFirst()] instanceof Function)
        {
            this['_init' + uri[0].ucFirst()](uri);
        }
        if (!(this.removeEventListener))
        {
            this.detachEvent('onload',loader);//(fix leak?
            return null;
        }
        this.removeEventListener('load',loader,false);
    }
    if (!(go.addEventListener))
    {
        go.attachEvent('onload',loader);//(IE...
    }
    else
    {
        go.addEventListener('load',loader,false);
    }
})(window);

那样,(on)load事件在window.load处理程序返回之前被解除绑定。根据IEJSLeaksDetector工具,我的应用程序没有泄漏。我对此感到满意。希望这个片段对你们中的某个人有所帮助——如果有人有改进这种方法的建议,请不要犹豫!祝福大家,感谢所有读过并尝试了上述内容的人!
PS:如果有人在乎,这里是ucFirst字符串方法:
if (!(String.prototype.ucFirst))
{
    String.prototype.ucFirst = function()
    {
        "use strict";
        return this.charAt(0).toUpperCase() + this.slice(1);
    };
}

IEJSLeaksDetector 对 IE 8 有用吗,还是只对比 IE 8 更早的版本有用? - Notre
这对于IE8很有用,因为它是与IE8同时开发的(并且在IE8发布时仍在积极开发中)。 - Elias Van Ootegem

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