【问题标题】:Finding JavaScript memory leaks with Chrome使用 Chrome 查找 JavaScript 内存泄漏
【发布时间】:2013-11-06 09:58:05
【问题描述】:

我创建了一个非常简单的测试用例,它创建了一个 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 memory leak fixing

Using DevTools

据我所见,它们代表了一些更强大的材料。然而,除了介绍 3 快照技术 的概念之外,我发现它们在实践知识方面提供的很少(对于像我这样的初学者)。 '使用 DevTools' 教程没有通过一个真实的例子来工作,所以它对事物的模糊和一般的概念描述并没有太大的帮助。至于“Gmail”示例:

所以你发现了一个漏洞。现在呢?

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

  • 如果不能轻易推断分配站点(即事件监听器):

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

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

读完之后,我发现自己更加困惑,而不是更少。再说一遍,它只是告诉我事,而不是如何去做。从我的角度来看,那里的所有信息要么太模糊,要么只对已经了解流程的人有意义。

下面的@Jonathan Naguin's answer 提出了其中一些更具体的问题。

【问题讨论】:

  • 我对在浏览器中测试内存使用情况一无所知,但如果您还没有看过,Addy Osmani's article about the Chrome web inspector 可能会有所帮助。
  • 谢谢你的建议,保罗。但是,当我在单击删除之前拍摄一个快照,然后在单击它之后拍摄另一个快照,然后选择“在快照 1 和 2 之间分配的对象”(如他的文章中所建议的那样)时,仍然存在 2000 多个对象。例如,有 4 个“HTMLButtonElement”条目,这对我来说毫无意义。真的,我不知道发生了什么。
  • doh,这听起来不是特别有用。可能是使用像 JavaScript 这样的垃圾收集语言,您并不是真的要在与测试一样精细的级别上验证您正在使用内存做什么。检查内存泄漏的更好方法可能是调用main 10,000 次而不是一次,然后查看最后是否会使用更多内存。
  • @PaulD.Waite 是的,也许。但在我看来,我仍然需要进行细粒度分析来确定问题到底是什么,而不仅仅是能够说(或不说):“好吧,这里某处存在内存问题”。而且我确实觉得我应该能够在如此精细的级别上使用他们的分析器......我只是不确定如何:(

标签: javascript google-chrome backbone.js memory-leaks


【解决方案1】:

三个快照技术是查找内存泄漏的一个很好的工作流程,Loreena Lee 和 Gmail 团队首先使用它来解决他们的一些内存问题。一般来说,这些步骤是:

  • 拍摄堆快照。
  • 做事。
  • 拍摄另一个堆快照。
  • 重复相同的内容。
  • 拍摄另一个堆快照。
  • 在快照 3 的“摘要”视图中筛选在快照 1 和 2 之间分配的对象。

对于您的示例,我已调整代码以显示此过程(您可以找到它here),将主干视图的创建延迟到开始按钮的单击事件。现在:

  • 运行 HTML(使用此 address 保存在本地)并拍摄快照。
  • 点击开始创建视图。
  • 再拍一张快照。
  • 点击删除。
  • 再拍一张快照。
  • 在快照 3 的“摘要”视图中筛选在快照 1 和 2 之间分配的对象。

现在您可以发现内存泄漏了!

您会注意到几种不同颜色的节点。红色节点没有来自 Javascript 的直接引用,但它们是活动的,因为它们是分离的 DOM 树的一部分。树中可能有一个从 Javascript 引用的节点(可能作为闭包或变量),但巧合的是阻止了整个 DOM 树被垃圾收集。

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

在您的特定页面中,您可以看到标记为红色的 HTML Div 元素。如果您展开该元素,您将看到它被“缓存”函数引用。

选择该行并在您的控制台中输入 $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;
}

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

文档:

【讨论】:

  • 感谢您的努力。事实上,教程中经常提到三快照技术。不幸的是,细节经常被遗漏。例如,我很欣赏在控制台中引入 $0 功能,这对我来说是新的 - 当然,我不知道它在做什么或你如何知道使用它($1 似乎没用,而 $2似乎做同样的事情)。其次,您怎么知道要突出显示 #button in function cache() 行而不是其他几十行中的任何一行?最后NodeListHTMLInputElement也有红色节点,但是我想不出来。
  • 您怎么知道cache 行包含信息而其他行没有?有许多分支的距离小于cache 一个。而且我不确定您是如何知道HTMLInputElementHTMLDivElement 的孩子。我看到它在其中被引用(“HTMLDivElement 中的本机”),但它也引用了自己和两个HTMLButtonElements,这对我来说没有意义。我当然感谢您为这个示例找到答案,但我真的不知道如何将其推广到其他问题。
  • 这很奇怪,我使用的是您的示例,但得到的结果与您的不同(来自您的屏幕截图)。不过,我非常感谢您的所有帮助。我想我现在已经足够了,当我有一个需要具体帮助的真实示例时,我将在这里创建一个新问题。再次感谢。
  • $0 的解释可以在这里找到:developer.chrome.com/devtools/docs/commandline-api#0-4
  • Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view. 是什么意思?
【解决方案2】:

这里有一个关于 jsfiddle 内存分析的提示:使用以下 URL 隔离您的 jsfiddle 结果,它会删除所有 jsfiddle 框架并仅加载您的结果。

http://jsfiddle.net/4QhR2/show/

在阅读以下文档之前,我一直无法弄清楚如何使用 Timeline 和 Profiler 来追踪内存泄漏。阅读标题为“对象分配跟踪器”的部分后,我能够使用“记录堆分配”工具,并跟踪一些分离的 DOM 节点。

我通过从 jQuery 事件绑定切换到使用 Backbone 事件委托解决了这个问题。据我了解,如果您致电 View.remove(),较新版本的 Backbone 会自动为您解除绑定事件。自己执行一些演示,它们设置了内存泄漏供您识别。如果您在学习了本文档后仍然不明白,请随时在此处提问。

https://developers.google.com/chrome-developer-tools/docs/javascript-memory-profiling

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 例如,如果我查看 jsfiddle 的堆快照,在单击“删除”之前,存在远远超过 100,000 个对象。我在哪里可以找到我的 jsfiddle 代码实际创建的对象?我认为Window/http://jsfiddle.net/4QhR2/show 可能有用,但它只是无穷无尽的功能。我不知道里面发生了什么。
    • @EleventyOne:我不会使用 jsFiddle。为什么不在您自己的计算机上创建一个文件进行测试?
    • @BlueSkies 我做了一个 jsfiddle,所以这里的人们可以使用相同的代码库工作。尽管如此,当我在自己的计算机上创建一个文件进行测试时,堆快照中仍然存在 50,000 多个对象。
    • @EleventyOne 一个堆快照无法让您了解是否存在内存泄漏。你至少需要两个。
    • 确实如此。我强调的是,当存在数千个对象时,要知道要寻找什么是多么困难。
    【解决方案4】:

    有来自谷歌的介绍视频,对发现 JavaScript 内存泄漏很有帮助。

    https://www.youtube.com/watch?v=L3ugr9BJqIs

    【讨论】:

      【解决方案5】:

      您还可以查看开发人员工具中的“时间轴”选项卡。记录您应用的使用情况并密切关注 DOM 节点和事件侦听器计数。

      如果内存图确实表明内存泄漏,那么您可以使用分析器找出泄漏的原因。

      【讨论】:

        【解决方案6】:

        您可能还想阅读:

        http://addyosmani.com/blog/taming-the-unicorn-easing-javascript-memory-profiling-in-devtools/

        它解释了 chrome 开发人员工具的使用,并就如何使用堆快照比较和可用的不同 hep 快照视图来确认和定位内存泄漏提供了一些分步建议。

        【讨论】:

          【解决方案7】:

          我赞成拍摄堆快照的建议,它们非常适合检测内存泄漏,chrome 在快照方面做得很好。

          在我攻读学位的研究项目中,我正在构建一个交互式 Web 应用程序,该应用程序必须生成大量构建在“层”中的数据,其中许多层将在 UI 中“删除”,但由于某种原因内存没有被释放,使用快照工具我能够确定 JQuery 一直在对象上保留引用(来源是当我试图触发 .load() 事件时,尽管超出了范围,仍然保留了引用) .手头上的这些信息单独保存了我的项目,当您使用其他人的库时,它是一个非常有用的工具,并且您会遇到延迟引用阻止 GC 完成其工作的问题。

          编辑: 提前计划您将要执行的操作以最大限度地减少拍摄快照所花费的时间、假设可能导致问题的原因并测试每个场景、在之前和之后制作快照也很有用。

          【讨论】:

            【解决方案8】:

            关于使用 Chrome 开发者工具识别内存泄漏的几个重要注意事项:

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

            2) 避免将 anything 记录到浏览器控制台。 Chrome 不会垃圾收集写入控制台的对象,因此会影响您的结果。您可以通过将以下代码放在脚本/页面的开头来禁止输出:

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

            3) 使用堆快照并搜索“分离”来识别分离的 DOM 元素。通过悬停对象,您可以访问所有属性,包括可能有助于识别每个元素的 idouterHTML 如果分离的元素仍然太通用而无法识别,请在运行测试之前使用浏览器控制台为其分配唯一 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" 识别分离元素时,重新加载您的页面,再次执行上面的 sn-p,然后使用 DOM 检查器或文档找到 id="AutoId_49" 的元素。查询选择器(..)。当然,这只有在您的页面内容是可预测的情况下才有效。

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

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

            2) 在页面上做一些可能导致内存泄漏的事情

            3) 使用开发者工具拍摄堆快照并搜索“分离”

            4) 悬停元素以从它们的 idouterHTML 属性中识别它们

            【讨论】:

            • 另外,禁用缩小/丑化总是一个好主意,因为它会使浏览器中的调试更加困难。
            【解决方案9】:

            使用 2021 年可用的工具在此处添加我的 2 美分:https://yonatankra.com/how-to-profile-javascript-performance-in-the-browser/

            这里有一个短视频版本:https://yonatankra.com/detect-memory-leak-with-chrome-dev-tools

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2014-03-26
              • 1970-01-01
              • 1970-01-01
              • 2011-07-08
              • 1970-01-01
              • 1970-01-01
              • 2017-11-29
              • 1970-01-01
              相关资源
              最近更新 更多