Garbage collection frees us up to focus on application logic (rather than memory management). However, garbage collection is not magical. Understanding how it works, and how to make it retain memory that should have been freed long ago, can lead to faster and more reliable applications. In this article, learn a systematic approach to locating memory leaks in JavaScript applications, several common leak patterns, and the appropriate methods to resolve these leaks.
1. Introduction
When dealing with a scripting language like JavaScript, it’s easy to forget that every object, class, string, number, and method needs to allocate and reserve memory. The specific details of memory allocation and deallocation are hidden from the language and runtime garbage collectors.
Many functions can be implemented without considering memory management, but ignoring it may cause significant problems in the program. Improperly cleaned objects may persist much longer than expected. These objects continue to respond to events and consume resources. They force the browser to allocate memory pages from a virtual disk drive, significantly slowing down the computer (and in extreme cases, causing the browser to crash).
A memory leak is any object that persists after you no longer own or need it. In recent years, many browsers have improved their ability to reclaim memory from JavaScript during page loading. However, not all browsers operate the same way. Both Firefox and older versions of Internet Explorer have experienced memory leaks that persist until the browser is closed.
Many classic patterns that caused memory leaks in the past no longer cause memory leaks in modern browsers. However, there is a different trend affecting memory leaks today. Many are designing web applications to run in a single page without hard page refreshes. In a single page like that, it's easy to retain memory that is no longer needed or relevant when moving from one state of the application to another.
In this article, learn about the basic life cycle of objects, how garbage collection determines whether an object has been freed, and how to evaluate potential leaking behavior. Plus, learn how to use Heap Profiler in Google Chrome to diagnose memory issues. Some examples show how to resolve memory leaks from closures, console logs, and loops.
2. Object life cycle
To understand how to prevent memory leaks, you need to understand the basic life cycle of objects. When an object is created, JavaScript automatically allocates appropriate memory for the object. From this moment on, the garbage collector continuously evaluates the object to see if it is still a valid object.
The garbage collector scans objects periodically and counts the number of other objects that have references to each object. If an object has 0 references (no other objects refer to it), or the only reference to the object is circular, then the object's memory can be reclaimed. Figure 1 shows an example of the garbage collector reclaiming memory.
Figure 1. Reclaiming memory through garbage collection
It would be helpful to see this system in action, but the tools to provide this functionality are limited. One way to know how much memory your JavaScript application is taking up is to use system tools to look at your browser's memory allocations. There are several tools that can provide you with current usage and trend graphs of a process's memory usage over time.
For example, if you have XCode installed on Mac OSX, you can launch the Instruments app and attach its Activity Monitor tool to your browser for real-time analysis. On Windows®, you can use Task Manager. You know you have a memory leak if you notice a steady increase in memory usage over time as you use your application.
Observing the browser's memory footprint only gives a very rough indication of the actual memory usage of a JavaScript application. Browser data doesn't tell you which objects were leaked, and there's no guarantee that the data actually matches your application's true memory footprint. Also, due to implementation issues in some browsers, DOM elements (or alternative application-level objects) may not be released when the corresponding element is destroyed in the page. This is especially true for video tags, which require browsers to implement a more elaborate infrastructure.
There have been many attempts to add tracking of memory allocations in client-side JavaScript libraries. Unfortunately, none of the attempts were particularly reliable. For example, the popular stats.js package is not supported due to inaccuracies. In general, trying to maintain or determine this information from the client is problematic because it introduces overhead into the application and cannot terminate reliably.
The ideal solution would be for the browser vendor to provide a set of tools within the browser that help you monitor memory usage, identify leaked objects, and determine why a particular object is still marked as reserved.
Currently, only Google Chrome (which provides Heap Profile) implements a memory management tool as its developer tools. In this article I use Heap Profiler to test and demonstrate how the JavaScript runtime handles memory.
3. Analyzing heap snapshots
Before creating a memory leak, look at a simple interaction that collects memory appropriately. Start by creating a simple HTML page with two buttons, as shown in Listing 1.
List 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 is included to ensure a simple syntax for managing event bindings that works across browsers and strictly adheres to the most common development practices. Add script tags to the leaker class and main JavaScript methods. In a development environment, it's often a better practice to combine JavaScript files into a single file. For the purposes of this example, it's easier to put the logic in a separate file.
You can filter the Heap Profiler to show only instances of a particular class. To take advantage of this feature, create a new class that encapsulates the behavior of leaking objects, and this class is easily found in the Heap Profiler, as shown in Listing 2.
List 2. assets/scripts/leaker.js
var Leaker = function(){}; Leaker.prototype = { init:function(){ } };
Bind the Start button to initialize the Leaker object and assign it to a variable in the global namespace. You also need to bind the Destroy button to a method that should clean up the Leaker object and prepare it for garbage collection, as shown in Listing 3.
List 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();
Now you are ready to create an object, view it in memory, and then release it.
1) Load the index page in Chrome. Because you're loading jQuery directly from Google, you'll need an Internet connection to run this sample.
2) Open the developer tools by opening the View menu and selecting the Develop submenu. Select the Developer Tools command.
3) Go to the Profiles tab and get a heap snapshot, as shown in Figure 2.
Figure 2. Profiles tab
4) Return your attention to the Web and select Start.
5). Get another heap snapshot.
6) Filter the first snapshot and look for instances of the Leaker class, but no instances can be found. Switch to the second snapshot and you should find an instance as shown in Figure 3.
Figure 3. Snapshot example
7) Return your attention to the Web and select Destroy.
8). Obtain the third heap snapshot.
9) Filter the third snapshot and look for instances of the Leaker class, but no instances can be found. While loading the third snapshot, you can also switch the analysis mode from Summary to Comparison and compare the third and second snapshots. You see an offset value of -1 (an instance of the Leaker object was released between snapshots).
Long live! Garbage collection is effective. Now it's time to destroy it.
4. Memory Leak 1: Closure
A simple way to prevent an object from being garbage collected is to set an interval or timeout for referencing the object in callbacks. To see it in action, update the leaker.js class, as shown in Listing 4.
List 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"); } };
Now, when repeating steps 1-9 from the previous section, you should see in the third snapshot that the Leaker object is persisted and the interval continues running forever. So what happened? Any local variables referenced within a closure are retained by the closure as long as the closure exists. To ensure that the callback to the setInterval method is executed when the scope of the Leaker instance is accessed, the this variable needs to be assigned to the local variable self, which is used to trigger onInterval from within the closure. When onInterval fires, it can access any instance variable in the Leaker object (including itself). However, as long as the event listener exists, the Leaker object will not be garbage collected.
To resolve this issue, trigger the destroy method added to the leaker object before clearing the stored reference to it, by updating the Destroy button's click handler, as shown in Listing 5.
List 5. assets/scripts/main.js
$("#destroy_button").click(function(){ leak.destroy(); leak = null; });
五、销毁对象和对象所有权
一种不错的做法是,创建一个标准方法来负责让一个对象有资格被垃圾回收。destroy 功能的主要用途是,集中清理该对象完成的具有以下后果的操作的职责:
1、阻止它的引用计数下降到 0(例如,删除存在问题的事件侦听器和回调,并从任何服务取消注册)。
2、使用不必要的 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)、在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。
2)、由 console.log 和 console.dir 方法记录的对象。
七、内存泄漏 3:循环
在两个对象彼此引用且彼此保留时,就会产生一个循环,如图 4 所示。
图 4. 创建一个循环的引用
该图中的一个蓝色 root 节点连接到两个绿色框,显示了它们之间的一个连接
清单 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 root return; } 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 out return; } this._subscribers.push(subscriber); }, remove:function(subscriber){ if(this._subscribers.indexOf(subscriber) < 0){ // Not currently registered so bail out return; } this._subscribers.splice( this._subscribers.indexOf(subscriber), 1 ); } };
registry 类是让其他对象向它注册,然后从注册表中删除自身的对象的简单示例。尽管这个特殊的类与注册表毫无关联,但这是事件调度程序和通知系统中的一种常见模式。
将该类导入 index.html 页面中,放在 leaker.js 之前,如清单 10 所示。
清单 10. index.html
charset="utf-8">
更新 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 root return; } 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 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。
在创建对象时要计划该对象的清理工作,这比在以后将一个清理阶段移植到应用程序中要容易得多。常常要计划删除事件侦听器,并停止您创建的间隔。如果认识到了您应用程序中的内存使用,您将得到更可靠且性能更高的应用程序。