深入理解JavaScript程序中内存泄漏(2)

<html> <head> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script> </head> <body> <button>Start</button> <button>Destroy</button> <script src="https://www.jb51.net/assets/scripts/leaker.js" type="text/javascript" charset="utf-8"></script> <script src="https://www.jb51.net/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 所示。

深入理解JavaScript程序中内存泄漏

图 2. Profiles 选项卡

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

深入理解JavaScript程序中内存泄漏

图 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; });

五、销毁对象和对象所有权

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wgyjxd.html