检测和解决内存问题

垃圾回收解放了我们,它让我们可将精力集中在应用程序逻辑(而不是内存管理)上。但是,垃圾收集并不神奇。了解它的工作原理,以及如何使它保留本应在很久以前释放的内存,就可以实现更快更可靠的应用程序。在本文中,学习一种定位 JavaScript 应用程序中内存泄漏的系统方法、几种常见的泄漏模式,以及解决这些泄漏的适当方法。


简介

当处理 JavaScript 这样的脚本语言时,很容易忘记每个对象、类、字符串、数字和方法都需要分配和保留内存。语言和运行时的垃圾回收器隐藏了内存分配和释放的具体细节。

许多功能无需考虑内存管理即可实现,但却忽略了它可能在程序中带来重大的问题。不当清理的对象可能会存在比预期要长得多的时间。这些对象继续响应事件和消耗资源。它们可强制浏览器从一个虚拟磁盘驱动器分配内存页,这显著影响了计算机的速度(在极端的情形中,会导致浏览器崩溃)。

内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。在最近几年中,许多浏览器都改善了在页面加载过程中从 JavaScript 回收内存的能力。但是,并不是所有浏览器都具有相同的运行方式。Firefox 和旧版的 Internet Explorer 都存在过内存泄漏,而且内存泄露一直持续到浏览器关闭。

过去导致内存泄漏的许多经典模式在现代浏览器中以不再导致泄漏内存。但是,如今有一种不同的趋势影响着内存泄漏。许多人正设计用于在没有硬页面刷新的单页中运行的 Web 应用程序。在那样的单页中,从应用程序的一个状态到另一个状态时,很容易保留不再需要或不相关的内存。

在本文中,了解对象的基本生命周期,垃圾回收如何确定一个对象是否被释放,以及如何评估潜在的泄漏行为。另外,学习如何使用 Google Chrome 中的 Heap Profiler 来诊断内存问题。一些示例展示了如何解决闭包、控制台日志和循环带来的内存泄漏。

您可 下载 本文中使用的示例的源代码。


对象生命周期

要了解如何预防内存泄漏,需要了解对象的基本生命周期。当创建一个对象时,JavaScript 会自动为该对象分配适当的内存。从这一刻起,垃圾回收器就会不断对该对象进行评估,以查看它是否仍是有效的对象。

垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。图 1 显示了垃圾回收器回收内存的一个示例。

图 1. 通过垃圾收集回收内存

看到该系统的实际应用会很有帮助,但提供此功能的工具很有限。了解您的 JavaScript 应用程序占用了多少内存的一种方式是使用系统工具查看浏览器的内存分配。有多个工具可为您提供当前的使用,并描绘一个进程的内存使用量随时间变化的趋势图。

例如,如果在 Mac OSX 上安装了 XCode,您可以启动 Instruments 应用程序,并将它的活动监视器工具附加到您的浏览器上,以进行实时分析。在 Windows® 上,您可以使用任务管理器。如果在您使用应用程序的过程中,发现内存使用量随时间变化的曲线稳步上升,那么您就知道存在内存泄漏。

观察浏览器的内存占用只能非常粗略地显示 JavaScript 应用程序的实际内存使用。浏览器数据不会告诉您哪些对象发生了泄漏,也无法保证数据与您应用程序的真正内存占用确实匹配。而且,由于一些浏览器中存在实现问题,DOM 元素(或备用的应用程序级对象)可能不会在页面中销毁相应元素时释放。视频标记尤为如此,视频标记需要浏览器实现一种更加精细的基础架构。

人们曾多次尝试在客户端 JavaScript 库中添加对内存分配的跟踪。不幸的是,所有尝试都不是特别可靠。例如,流行的 stats.js 包由于不准确性而无法支持。一般而言,尝试从客户端维护或确定此信息存在一定的问题,是因为它会在应用程序中引入开销且无法可靠地终止。

理想的解决方案是浏览器供应商在浏览器中提供一组工具,帮助您监视内存使用,识别泄漏的对象,以及确定为什么一个特殊对象仍标记为保留。

目前,只有 Google Chrome(提供了 Heap Profile)实现了一个内存管理工具作为它的开发人员工具。我在本文中使用 Heap Profiler 测试和演示 JavaScript 运行时如何处理内存。


分析堆快照

在创建内存泄漏之前,请查看一次适当收集内存的简单交互。首先创建一个包含两个按钮的简单 HTML 页面,如清单 1 所示。

清单 1. index.html
<html>
<head><script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"
type="text/javascript"></script>
</head>
<body><button id="start_button">Start</button><button id="destroy_button">Destroy</button><script src="assets/scripts/leaker.js" type="text/javascript"
charset="utf-8"></script><script src="assets/scripts/main.js" type="text/javascript"
charset="utf-8"></script>
</body>
</html>

包含 jQuery 是为了确保一种管理事件绑定的简单语法适合不同的浏览器,而且严格遵守最常见的开发实践。为 leaker 类和主要 JavaScript 方法添加脚本标记。在开发环境中,将 JavaScript 文件合并到单个文件中通常是一种更好的做法。出于本示例的用途,将逻辑放在独立的文件中更容易。

您可以过滤 Heap Profiler 来仅显示特殊类的实例。为了利用该功能,创建一个新类来封装泄漏对象的行为,而且这个类很容易在 Heap Profiler 中找到,如清单 2 所示。

清单 2. assets/scripts/leaker.js
var Leaker = function(){};
Leaker.prototype = {init:function(){}
};

绑定 Start 按钮以初始化 Leaker 对象,并将它分配给全局命名空间中的一个变量。还需要将 Destroy 按钮绑定到一个应清理 Leaker 对象的方法,并让它为垃圾收集做好准备,如清单 3 所示。

清单 3. assets/scripts/main.js
$("#start_button").click(function(){if(leak !== null || leak !== undefined){return;}leak = new Leaker();leak.init();
});$("#destroy_button").click(function(){leak = null;
});var leak = new Leaker();

现在,您已准备好创建一个对象,在内存中查看它,然后释放它。

  1. 在 Chrome 中加载索引页面。

    因为您是直接从 Google 加载 jQuery,所以需要连接互联网来运行该样例。

  2. 打开开发人员工具,方法是打开 View 菜单并选择 Develop 子菜单。选择 Developer Tools 命令。
  3. 转到 Profiles 选项卡并获取一个堆快照,如图 2 所示。
    图 2. Profiles 选项卡

  4. 将注意力返回到 Web 上,选择 Start
  5. 获取另一个堆快照。
  6. 过滤第一个快照,查找 Leaker 类的实例,找不到任何实例。切换到第二个快照,您应该能找到一个实例,如图 3 所示。
    图 3. 快照实例

  7. 将注意力返回到 Web 上,选择 Destroy
  8. 获取第三个堆快照。
  9. 过滤第三个快照,查找 Leaker 类的实例,找不到任何实例。

    在加载第三个快照时,也可将分析模式从 Summary 切换到 Comparison,并对比第三个和第二个快照。您会看到偏移值 -1(在两次快照之间释放了 Leaker 对象的一个实例)。

万岁!垃圾回收有效的。现在是时候破坏它了。


内存泄漏 1:闭包

一种预防一个对象被垃圾回收的简单方式是设置一个在回调中引用该对象的间隔或超时。要查看实际应用,可更新 leaker.js 类,如清单 4 所示。

清单 4. assets/scripts/leaker.js
var Leaker = function(){};Leaker.prototype = {init:function(){this._interval = null;this.start();},start: function(){var self = this;this._interval = setInterval(function(){self.onInterval();}, 100);},destroy: function(){if(this._interval !== null){clearInterval(this._interval);          }},onInterval: function(){console.log("Interval");}
};

现在,当重复 上一节 中的第 1-9 步时,您应在第三个快照中看到,Leaker 对象被持久化,并且该间隔会永远继续运行。那么发生了什么?在一个闭包中引用的任何局部变量都会被该闭包保留,只要该闭包存在就永远保留。要确保对 setInterval 方法的回调在访问 Leaker 实例的范围时执行,需要将 this 变量分配给局部变量 self,这个变量用于从闭包内触发 onInterval。当 onInterval 触发时,它就能够访问Leaker 对象中的任何实例变量(包括它自身)。但是,只要事件侦听器存在,Leaker 对象就不会被垃圾回收。

要解决此问题,可在清空所存储的 leaker 对象引用之前,触发添加到该对象的 destroy 方法,方法是更新 Destroy 按钮的单击处理程序,如清单 5 所示。

清单 5. assets/scripts/main.js
$("#destroy_button").click(function(){leak.destroy();leak = null;
});


销毁对象和对象所有权

一种不错的做法是,创建一个标准方法来负责让一个对象有资格被垃圾回收。destroy 功能的主要用途是,集中清理该对象完成的具有以下后果的操作的职责:

  • 阻止它的引用计数下降到 0(例如,删除存在问题的事件侦听器和回调,并从任何服务取消注册)。
  • 使用不必要的 CPU 周期,比如间隔或动画。

destroy 方法常常是清理一个对象的必要步骤,但在大多数情况下它还不够。在理论上,在销毁相关实例后,保留对已销毁对象的引用的其他对象可调用自身之上的方法。因为这种情形可能会产生不可预测的结果,所以仅在对象即将无用时调用 destroy 方法,这至关重要。

一般而言,destroy 方法最佳使用是在一个对象有一个明确的所有者来负责它的生命周期时。此情形常常存在于分层系统中,比如 MVC 框架中的视图或控制器,或者一个画布呈现系统的场景图。


内存泄漏 2:控制台日志

一种将对象保留在内存中的不太明显的方式是将它记录到控制台中。清单 6 更新了 Leaker 类,显示了此方式的一个示例。

清单 6. assets/scripts/leaker.js
var Leaker = function(){};Leaker.prototype = {init:function(){console.log("Leaking an object: %o", this);},destroy: function(){}
};

可采取以下步骤来演示控制台的影响。

  1. 登录到索引页面。
  2. 单击 Start
  3. 转到控制台并确认 Leaking 对象已被跟踪。
  4. 单击 Destroy
  5. 回到控制台并键入 leak,以记录全局变量当前的内容。此刻该值应为空。
  6. 获取另一个堆快照并过滤 Leaker 对象。

    您应留下一个 Leaker 对象。

  7. 回到控制台并清除它。
  8. 创建另一个堆配置文件。

    在清理控制台后,保留 leaker 的配置文件应已清除。

控制台日志记录对总体内存配置文件的影响可能是许多开发人员都未想到的极其重大的问题。记录错误的对象可以将大量数据保留在内存中。注意,这也适用于:

  • 在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。
  • 由 console.log 和 console.dir 方法记录的对象。

内存泄漏 3:循环

在两个对象彼此引用且彼此保留时,就会产生一个循环,如图 4 所示。

图 4. 创建一个循环的引用

清单 7 显示了一个简单的代码示例。

清单 7. assets/scripts/leaker.js
var Leaker = function(){};Leaker.prototype = {init:function(name, parent){this._name = name;this._parent = parent;this._child = null;this.createChildren();},createChildren:function(){if(this._parent !== null){// Only create a child if this is the rootreturn;}this._child = new Leaker();this._child.init("leaker 2", this);},destroy: function(){}
};

Root 对象的实例化可以修改,如清单 8 所示。

清单 8. assets/scripts/main.js
leak = new Leaker();
leak.init("leaker 1", null);

如果在创建和销毁对象后执行一次堆分析,您应该会看到垃圾收集器检测到了这个循环引用,并在您选择 Destroy 按钮时释放了内存。

但是,如果引入了第三个保留该子对象的对象,该循环会导致内存泄漏。例如,创建一个 registry 对象,如清单 9 所示。

清单 9. assets/scripts/registry.js
var Registry = function(){};Registry.prototype = {init:function(){this._subscribers = [];},add:function(subscriber){if(this._subscribers.indexOf(subscriber) >= 0){// Already registered so bail outreturn;}this._subscribers.push(subscriber);},remove:function(subscriber){if(this._subscribers.indexOf(subscriber) < 0){// Not currently registered so bail outreturn;}this._subscribers.splice(this._subscribers.indexOf(subscriber), 1);}
};

registry 类是让其他对象向它注册,然后从注册表中删除自身的对象的简单示例。尽管这个特殊的类与注册表毫无关联,但这是事件调度程序和通知系统中的一种常见模式。

将该类导入 index.html 页面中,放在 leaker.js 之前,如清单 10 所示。

清单 10. index.html
<script src="assets/scripts/registry.js" type="text/javascript"
charset="utf-8"></script>

更新 Leaker 对象,以向注册表对象注册该对象本身(可能用于有关一些未实现事件的通知)。这创建了一个来自要保留的 leaker 子对象的 root 节点备用路径,但由于该循环,父对象也将保留,如清单 11 所示。

清单 11. assets/scripts/leaker.js
var Leaker = function(){};
Leaker.prototype = {init:function(name, parent, registry){this._name = name;this._registry = registry;this._parent = parent;this._child = null;this.createChildren();this.registerCallback();},createChildren:function(){if(this._parent !== null){// Only create child if this is the rootreturn;}this._child = new Leaker();this._child.init("leaker 2", this, this._registry);},registerCallback:function(){this._registry.add(this);},destroy: function(){this._registry.remove(this);}
};

最后,更新 main.js 以设置注册表,并将对注册表的一个引用传递给 leaker 父对象,如清单 12 所示。

清单 12. assets/scripts/main.js
      $("#start_button").click(function(){var leakExists = !(window["leak"] === null || window["leak"] === undefined);if(leakExists){return;}leak = new Leaker();leak.init("leaker 1", null, registry);
});$("#destroy_button").click(function(){leak.destroy();leak = null;
});registry = new Registry();
registry.init();

现在,当执行堆分析时,您应看到每次选择 Start 按钮时,会创建并保留 Leaker 对象的两个新实例。图 5 显示了对象引用的流程。

图 5. 由于保留引用导致的内存泄漏

从表面上看,它像一个不自然的示例,但它实际上非常常见。更加经典的面向对象框架中的事件侦听器常常遵循类似图 5 的模式。这种类型的模式也可能与闭包和控制台日志导致的问题相关联。

尽管有多种方式来解决此类问题,但在此情况下,最简单的方式是更新 Leaker 类,以在销毁它时销毁它的子对象。对于本示例,更新destroy 方法(如清单 13 所示)就足够了。

清单 13. assets/scripts/leaker.js
destroy: function(){if(this._child !== null){this._child.destroy();            }this._registry.remove(this);
}

有时,两个没有足够紧密关系的对象之间也会存在循环,其中一个对象管理另一个对象的生命周期。在这样的情况下,在这两个对象之间建立关系的对象应负责在自己被销毁时中断循环。


结束语

即使 JavaScript 已被垃圾回收,仍然会有许多方式会将不需要的对象保留在内存中。目前大部分浏览器都已改进了内存清理功能,但评估您应用程序内存堆的工具仍然有限(除了使用 Google Chrome)。通过从简单的测试案例开始,很容易评估潜在的泄漏行为并确定是否存在泄漏。

不经过测试,就不可能准确度量内存使用。很容易使循环引用占据对象曲线图中的大部分区域。Chrome 的 Heap Profiler 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。

在创建对象时要计划该对象的清理工作,这比在以后将一个清理阶段移植到应用程序中要容易得多。常常要计划删除事件侦听器,并停止您创建的间隔。如果认识到了您应用程序中的内存使用,您将得到更可靠且性能更高的应用程序。


参考资料

学习

  • Chrome Developer Tools: Heap Profiling:借助此教程学习如何使用 Heap Profiler 揭示您的应用程序中的内存泄漏。
  • “JavaScript 中的内存泄漏模式”(developerWorks,2007 年 4 月):了解 JavaScript 中的循环引用的基本知识,以及为什么它们会在某些浏览器中引发问题。
  • “查找内存泄漏”:了解即使在不了解源代码的情况下也可以轻松地诊断泄漏的方式。
  • “JavaScript 内存泄漏”:了解有关内存泄漏的原因和检测的更多信息。
  • “avaScript and the Document Object Model”(developerWorks,2002 年 7 月):了解 JavaScript 的 DOM 方法,以及如何构建一个可以让用户添加备注和和编辑备注内容的网页。
  • A re-introduction to JavaScript:更详细地了解 JavaScript 及其特性。
  • developerWorks Web 开发专区:查找涉及各种基于 Web 的解决方案的文章。访问 Web 开发技术库,查阅丰富的技术文章,以及技巧、教程、标准和 IBM 红皮书。
  • developerWorks 技术活动和网络广播:随时关注这些会议中的技术。
  • developerWorks 点播演示:观看丰富的演示,包括面向初学者的产品安装和设置,以及为经验丰富的开发人员提供的高级功能。
  • Twitter 上的 developerWorks:立即加入以关注 developerWorks 推文。

获得产品和技术

  • 开发人员频道:获取 Google Chrome 版本以及最新的 Developer Tools 版本。
  • IBM 产品评估版:下载或 浏览 IBM SOA 沙盒中的在线教程,亲自使用来自 DB2、Lotus、Rational、Tivoli 和 WebSphere 的应用程序开发工具和中间件产品。

讨论

  • developerWorks 社区:查看开发人员推动的博客、论坛、群组和维基,并与其他 developerWorks 用户交流。

转自:http://www.ibm.com/developerworks/cn/web/wa-jsmemory/

转载于:https://www.cnblogs.com/duhuo/p/4759770.html

了解 JavaScript 应用程序中的内存泄漏相关推荐

  1. Java应用程序中的内存泄漏和内存管理

    Java平台最突出的功能之一是其自动内存管理. 许多人错误地将此功能转换为Java中没有内存泄漏 . 但是,事实并非如此,我给人的印象是,现代Java框架和基于Java的平台,尤其是Android平台 ...

  2. 监视和检测Java应用程序中的内存泄漏

    因此,您的应用程序内存不足,您日夜不停地分析应用程序,以期捕获对象中的内存漏洞. 后续步骤将说明如何监视和检测您的内存泄漏,以确保您的应用程序安全. 1.怀疑内存泄漏 如果您怀疑有内存泄漏,可以使用一 ...

  3. 翻译 | 理解Java中的内存泄漏

    猪年第一篇译文,大家多多支持! 原文自工程师baeldung博客,传送门 1. 介绍 Java 的其中一个核心特点是经由内置的垃圾回收机制(GC)下的自动化内存管理.GC 默默地处理着内存分配和释放工 ...

  4. java应用程序占用高内存_对Java应用程序中的内存问题进行故障排除

    java应用程序占用高内存 重要要点 解决内存问题可能很棘手,但是正确的方法和正确的工具集可以大大简化此过程. Java HotSpot JVM可以报告几种OutOfMemoryError消息,因此务 ...

  5. 谈谈android中的内存泄漏

    写在前面 内存泄漏实际上很多时候,对于开发者来说不容易引起重视.因为相对于crash来说,android中一两个地方发生内存泄漏的时候,对于整体没有特别严重的影响.但是我想说的是,当内存泄漏多的时候, ...

  6. 了解Java中的内存泄漏

    来源:SpringForAll社区 1. 简介 Java的核心优势之一是在内置垃圾收集器(简称GC)的帮助下实现自动内存管理.GC隐含地负责分配和释放内存,因此能够处理大多数内存泄漏问题. 虽然GC有 ...

  7. 介绍Java中的内存泄漏

    转载自  介绍Java中的内存泄漏 Java语言的一个关键的优势就是它的内存管理机制.你只管创建对象,Java的垃圾回收器帮你分配以及回收内存.然而,实际的情况并没有那么简单,因为内存泄漏在Java应 ...

  8. new arraylist内存_如何避免内部类中的内存泄漏

    我先假设读者已经熟悉在Java代码中使用嵌套类的基础知识.在本文里,我将展示嵌套类的陷阱,内部类在JVM中引起内存泄漏和内存不足错误的地方.之所以会发生这种类型的内存泄漏,是因为内部类必须始终能够访问 ...

  9. java final 内存泄漏_干货详解:一文教你如何避免内部类中的内存泄漏

    我先假设读者已经熟悉在Java代码中使用嵌套类的基础知识.在本文里,我将展示嵌套类的陷阱,内部类在JVM中引起内存泄漏和内存不足错误的地方.之所以会发生这种类型的内存泄漏,是因为内部类必须始终能够访问 ...

  10. Java中关于内存泄漏分析和解决方案,都在这里了!

    作者:李序锴 www.jianshu.com/p/54b5da7c6816 最近正在熟悉Java内存泄漏的相关知识,上网查阅了一些资料,在此做个整理算是对收获的一些总结,希望能对各位有所帮助,有问题可 ...

最新文章

  1. android与PC,C#与Java 利用protobuf 进行无障碍通讯【Socket】
  2. php 画布插入图像,javascript – 如何在PHP中将html5画布图像保存到数据库
  3. 面向对象编程设计模式--简单工厂模式讲解(历史上最简单明白的例子)
  4. mysql ubuntu 17.04_1、mysql 5.7 ubuntu17.04
  5. 【实验记录】EA-MLP(演化算法--全连接神经网络)实验记录
  6. linux php文件,如何在linux执行PHP文件
  7. MySQL基础练习题(带答案)
  8. 许巍的故乡到底想表达什么?是写给谁的?
  9. 基于C++和OpenCV的中心线提取算法
  10. 分布式原理:一文了解 Gossip 协议
  11. (转)C#中两个问号和一个问号 ??
  12. XMind (2021)新增演说模式
  13. 会计科目 分类 说明
  14. SLAM发展现状研究
  15. 回复热爱计算机的数控生
  16. LPR个人房贷利率如何计算?(商贷部分)
  17. 期货换手多换空换(期货交易多换空换是什么意思)
  18. 密码必须符合复杂性要求
  19. 【mud】object增加颜色的代码以及参考
  20. 微信小程序中自定义函数的学习使用

热门文章

  1. ThoughtWorks 2018技术雷达峰会(6月2日@深圳)
  2. linux进阶之gitlab仓库搭建及免密使用
  3. python + selenium 获取标签文本的为空解决办法
  4. Qt::FocusPolicy的使用
  5. 【DSP开发】DSP程序优化
  6. $(function() {});和$(document).ready(function() {});区别
  7. jQuery源码分析系列:Deferred延迟队列
  8. 内容超过7行显示查看全文
  9. note 2 运算符和表达式
  10. prototype 对象的进一步深入理解