使用Chrome查找JavaScript内存泄漏

177
我创建了一个非常简单的测试案例,它创建了一个Backbone视图,将处理程序附加到事件上,并实例化了一个用户定义的类。我相信通过在此示例中点击“删除”按钮,一切都将被清除,不应该有内存泄漏。
这里是代码的jsfiddle链接:http://jsfiddle.net/4QhR2/
// scope everything to a function
function main() {

    function MyWrapper() {
        this.element = null;
    }
    MyWrapper.prototype.set = function(elem) {
        this.element = elem;
    }
    MyWrapper.prototype.get = function() {
        return this.element;
    }

    var MyView = Backbone.View.extend({
        tagName : "div",
        id : "view",
        events : {
            "click #button" : "onButton",
        },    
        initialize : function(options) {        
            // done for demo purposes only, should be using templates
            this.html_text = "<input type='text' id='textbox' /><button id='button'>Remove</button>";        
            this.listenTo(this,"all",function(){console.log("Event: "+arguments[0]);});
        },
        render : function() {        
            this.$el.html(this.html_text);

            this.wrapper = new MyWrapper();
            this.wrapper.set(this.$("#textbox"));
            this.wrapper.get().val("placeholder");

            return this;
        },
        onButton : function() {
            // assume this gets .remove() called on subviews (if they existed)
            this.trigger("cleanup");
            this.remove();
        }
    });

    var view = new MyView();
    $("#content").append(view.render().el);
}

main();

然而,我不清楚如何使用Google Chrome的分析器来验证这是否是事实。在堆分析器快照中会出现无数东西,我不知道如何解码什么是好的/坏的。到目前为止我看到的教程要么只是告诉我“使用快照分析器”,要么就给我一个非常详细的宣言,介绍整个分析器是如何工作的。我能否只把分析器当做一个工具使用,或者我真的必须了解整个工程过程呢?

编辑: 例如这些教程:

修复Gmail内存泄漏

使用开发工具

根据我所见,这些代表了一些更强大的材料。然而,除了介绍“3快照技术”的概念外,我发现它们在实用知识方面(对于像我这样的初学者)提供的帮助非常有限。 “使用DevTools”教程没有通过真实示例进行讲解,因此其对事物的模糊和一般性描述并不是非常有用。至于“Gmail”示例:

那么你发现了一个泄漏,现在该怎么办?

  • 检查Profiles面板下泄漏对象的保留路径

  • 如果无法轻松推断分配站点(即事件侦听器):

  • 通过JS控制台仪器化保留对象的构造函数,以保存分配的堆栈跟踪

  • 正在使用Closure? 启用适当的现有标志(即goog.events.Listener.ENABLE_MONITORING)以在构建期间设置creationStack属性

我读完后反而更加困惑了,没有更清晰的理解。再次强调,它只是告诉我要做什么,而不是如何做。在我看来,所有的信息都太模糊了,或者只有那些已经理解该过程的人才能理解。下面@Jonathan Naguin's answer中提出了一些更具体的问题。

2
我对于在浏览器中测试内存使用方面一无所知,但是如果你还没有看过的话,Addy Osmani关于Chrome Web Inspector的文章或许会有所帮助。链接 - Paul D. Waite
1
谢谢你的建议,Paul。然而,在我点击“移除”之前拍了一个快照,然后在它被点击后再拍了一张快照,并选择“在快照1和2之间分配的对象”(正如他文章中建议的),仍然有超过2000个对象存在。例如,有4个“HTMLButtonElement”条目,这对我来说毫无意义。实际上,我不知道发生了什么。 - EleventyOne
3
咦,这听起来并不是特别有帮助。可能在像JavaScript这样的垃圾回收语言中,你并不需要像测试中那样以粒度级别验证内存使用情况。检查内存泄漏的更好方法可能是调用main 10,000 次而不是一次,然后看最终是否有更多的内存正在被使用。 - Paul D. Waite
3
@PaulD.Waite 嗯,也许吧。但我觉得我仍然需要进行细粒度的分析来确定问题所在,而不仅仅是能够说出或不能说出:“好的,在这里有一个内存问题”。而且我确实有印象应该能够在这样的细粒度级别上使用他们的性能分析工具……只是我不确定怎么做 :( - EleventyOne
1
你应该看一下 https://www.youtube.com/watch?v=L3ugr9BJqIs - maja
9个回答

224

寻找内存泄漏的好方法是“三次快照”技术,由Loreena Lee和Gmail团队首先使用来解决他们的一些内存问题。一般步骤如下:

  • 拍摄堆快照。
  • 进行操作。
  • 再次拍摄堆快照。
  • 重复相同操作。
  • 再次拍摄堆快照。
  • 在第3个“摘要”视图中过滤在第1个和第2个快照之间分配的对象。

针对您的示例,我已经调整了代码以展示此过程(可以在此处找到)。将Backbone View的创建延迟到单击开始按钮的事件。

  • 运行HTML(本地保存或使用此地址),然后拍摄一个快照。
  • 单击“Start”创建视图。
  • 再次拍摄堆快照。
  • 单击删除。
  • 再次拍摄堆快照。
  • 在第3个“摘要”视图中过滤在第1个和第2个快照之间分配的对象。

现在你已经准备好寻找内存泄漏了!

你会注意到有几个不同颜色的节点。红色节点在Javascript中没有直接引用,但是因为它们是一个分离的DOM树的一部分而存在。可能在树中有一个节点被Javascript引用(如闭包或变量),但恰好阻止了整个DOM树进行垃圾回收。

enter image description here

然而,黄色节点确实直接从Javascript引用。在相同的分离DOM树中查找黄色节点,以定位来自您的Javascript的引用。应该有一条属性链从DOM窗口指向元素。

在你这个例子中,你可以看到一个HTML Div元素标记为红色。如果您展开该元素,您将看到它被“cache”函数引用。

enter image description here

选择该行,在控制台中输入$0,您将看到实际函数和位置:

>$0
function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if ( keys.push( key += " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            delete cache[ keys.shift() ];
        }
        return (cache[ key ] = value);
    }                                                     jquery-2.0.2.js:1166

这是正在引用您元素的位置。不幸的是,您无法做太多事情,因为它是 jQuery 的内部机制。但是,仅供测试目的,请转到该函数并更改方法为:

function cache( key, value ) {
    return value;
}

现在如果您重复这个过程,您将不会看到任何红色节点 :)

文档:


8
感谢您的努力。确实,三个快照技术经常在教程中提到。不幸的是,细节经常被省略。例如,我很感激在控制台中引入$0函数,这对我来说是新的 - 当然,我不知道它在做什么或者你是如何知道使用它的($1似乎没用,而$2似乎做了相同的事情)。其次,你怎么知道要突出显示cache()函数中的#button行而不是其他几十行中的任何一行?最后,在NodeListHTMLInputElement中也有红色节点,但我无法理解它们的含义。 - EleventyOne
7
你是如何知道cache行包含信息而其他行没有的?有许多分支比cache行距离更短,我也不确定你是如何知道HTMLInputElementHTMLDivElement的子元素。我看到它在其中被引用(“HTMLDivElement”中本地),但它也引用了自身和两个HTMLButtonElement,这对我来说毫无意义。我当然感激您为此示例确定答案,但我真的不知道如何将其推广到其他问题。 - EleventyOne
2
很奇怪,我正在使用你的例子,但得到了和你截图不同的结果。尽管如此,我非常感激你所提供的所有帮助。我想现在已经够了,当我有一个需要特别帮助的现实例子时,我会在这里创建一个新的问题。再次感谢。 - EleventyOne
2
$0的解释可以在这里找到:https://developer.chrome.com/devtools/docs/commandline-api#0-4 - Sukrit Gupta
8
“在快照3的“摘要”视图中筛选在快照1和2之间分配的对象。” - basickarl
显示剩余13条评论

8
以下是关于jsfiddle内存分析的提示:使用以下URL来隔离jsfiddle结果,它会移除所有jsfiddle框架并仅加载你的结果。http://jsfiddle.net/4QhR2/show/。在阅读了相关文档中的“对象分配跟踪器”部分后,我才能够使用“记录堆分配”工具追踪一些分离的DOM节点。通过切换使用Backbone事件委托而非jQuery事件绑定,我解决了这个问题。据我所知,Backbone的新版本将在调用View.remove()时自动解绑事件。如果你仍然无法理解,请执行一些演示并寻求帮助。有关更多信息,请参见https://developers.google.com/chrome-developer-tools/docs/javascript-memory-profiling

7

基本上,你需要查看堆快照中的对象数量。如果两个快照之间对象数量增加并且你已经处理了对象,则会发生内存泄漏。我的建议是查找代码中未被分离的事件处理程序。


3
比如,如果我查看 jsfiddle 的堆快照,在点击“Remove”之前,会有超过十万个对象存在。那么我应该在哪里查找我的 jsfiddle 代码实际创建的对象?我认为 Window/http://jsfiddle.net/4QhR2/show 可能有用,但那里只有无尽的函数。我不知道里面发生了什么。 - EleventyOne
@EleventyOne:我不会使用jsFiddle。为什么不在自己的电脑上创建一个文件进行测试呢? - Blue Skies
1
@BlueSkies 我创建了一个 jsfiddle,这样大家就可以从同一代码库中工作。然而,当我在自己的计算机上创建一个用于测试的文件时,堆快照中仍然存在 50,000 多个对象。 - EleventyOne
@EleventyOne 一个堆快照并不能告诉你是否存在内存泄漏,至少需要两个。 - Konstantin Dinev
2
确实。我强调了当存在成千上万的对象时,要知道该寻找什么是多么困难。 - EleventyOne

6

4

4

关于使用Chrome开发者工具来识别内存泄漏,有几个重要注意点:

1)Chrome本身存在某些元素的内存泄漏问题,例如密码和数字字段。https://bugs.chromium.org/p/chromium/issues/detail?id=967438。在调试时避免使用这些元素,因为它们会在搜索已分离元素时污染您的堆快照。

2)不要将 任何东西 记录到浏览器控制台中。Chrome不会垃圾回收写入控制台的对象,因此影响您的结果。您可以通过在脚本/页面开头放置以下代码来抑制输出:

console.log = function() {};
console.warn = console.log;
console.error = console.log;

3)使用堆快照并搜索“detach”以识别已分离的DOM元素。通过悬停鼠标在对象上,您可以访问所有属性,包括idouterHTML,这可能有助于识别每个元素。 JS堆快照屏幕截图,显示有关已分离DOM元素的详细信息 如果无法识别分离的元素,请使用浏览器控制台为其分配唯一的ID,然后再运行测试,例如:

var divs = document.querySelectorAll("div");
for (var i = 0 ; i < divs.length ; i++)
{
    divs[i].id = divs[i].id || "AutoId_" + i;
}
divs = null; // Free memory

现在,当您使用id="AutoId_49"标识一个分离的元素时,请重新加载页面,再次执行上面的代码片段,并使用DOM检查器或document.querySelector(..)找到具有id="AutoId_49"的元素。前提是您的页面内容是可预测的。

如何运行测试以识别内存泄漏

1)加载页面(不输出控制台!)

2)在页面上进行可能导致内存泄漏的操作

3)使用开发人员工具获取堆快照并搜索“detach”

4)悬停元素以通过它们的idouterHTML属性来识别它们


此外,禁用代码压缩/混淆通常是一个好主意,因为它会使在浏览器中进行调试更加困难。 - Jimmy Thomsen

3

您还可以查看开发者工具中的时间轴选项卡。记录应用程序的使用情况并关注DOM节点和事件监听器的数量。

如果内存图确实显示存在内存泄漏,则可以使用分析器找出是什么在泄漏。


2
我赞同拍摄堆快照的建议,它们非常适用于检测内存泄漏,Chrome在拍摄快照方面做得非常出色。
在我为学位撰写的研究项目中,我正在构建一个交互式Web应用程序,需要生成大量的数据,并以“层”进行构建。其中许多层将在UI中“删除”,但由于某种原因,内存没有被释放。使用快照工具,我能够确定JQuery一直在对象上保持引用(源是当我尝试触发.load()事件时,引用仍然存在,尽管已经超出了范围)。有了这些信息,我的项目得到了拯救,当您使用其他人的库并且存在悬挂引用阻止GC执行其工作时,它是一个非常有用的工具。
编辑: 计划提前要执行的操作以最小化拍摄快照所需的时间,假设可能会导致问题并测试每个场景,在之前和之后进行快照也很有用。

-1

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