如何处理4种常见的内存泄露
什么是内存泄露
本质上来说,内存泄露可以被定义为 当内存不再被应用使用,却因为某些原因没有被操作系统回收或释放到自由内存池
编程语言支持多种不同的内存管理。某些内存是否在使用实际上是不可判定的。换句话说,只要开发者才清楚哪些内存块是可以被操作系统回收。
某些编程语言提供一些特性帮助开发者进行内存回收。其他的期望开发者完全明白哪些内存是无用的。维基有一系列关于内存管理的好文章。
四种常见的内存泄露
1. 全局变量
javascript 通过一个有趣的方式处理未声明的变量:引用未声明的变量会在全局对象里创建一个变量。在浏览器中,全局对象是window
, 换句话说:
function foo(arg) {
bar = "some text";
}
等价于:
function foo(arg) {
window.bar = "some text";
}
如果 bar
预想中是 foo
函数作用域内的局部变量,但又忘记使用 var
去声明,那么预料之外的全局变量就被创建了。
在这个例子中,一个简单的字符串泄露不会有多大害处,但这种写法当然会有更糟情况。
另一个意外创建全局变量的方式是通过 this
:
function foo() {
this.var1 = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();
为了防止这种情况发生,需要在 javascript 文件的开头添加
'use strict'
.这会使用严格模式解析javascript以防止意外的全局变量。了解更多javascript严格模式的执行情况
尽管我们在谈论意外的全局变量,但仍然会有无数的明确的全局变量。这些被定义为不可回收的(除非指定为null或重新分配)。特别是应该关注用于临时存储和处理大量数据的全局变量。如果必须要用全局变量去存储大量数据,当你处理完之后,请勿必记得指定为null或重新全配
2. 被遗忘的定时器和回调
在 javascript 中使用 setInterval
非常常见。
大部分类库提供观察者和其他调用回调的工具,小心地引用那些 拥有会变得不可访问的实例 的回调。setInterval
的例子,尽管这很常见:
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //This will be executed every ~5 seconds.
这个例子说明了定时器会发生的事:定时器引用了不再需要的node节点和数据。
被 renderer
引用的对象在将来某个时刻可能会被删除,让整个内部块都变得不必要了。但是,因为定时器还在运行,所以处理程序不能被回收(定时器需要被停止才能回收)。如果定时处理程序不能被回收,那么相关的一切都不能被回收。这意味着可能存储着大量数据的 serverData
也不能被回收。
观察者的例子中,很重要的一点是当不在需要的时候,必须明确移除他们(或者让相关的对象变得不可访问)。
在过去,这对于某些不能很好地管理内存的浏览器(像IE6)是非常重要的。如今,大部分浏览器有能力并且会回收不可访问的观察者处理程序,就算有时监听器没有被明确地移除。这仍然是很好的做法,不管怎样,在对象被处理掉之前明确地移除观察者。举个实例:
var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
counter++;
element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers // that don't handle cycles well.
框架或类库,比如 jQuery 在删除 node 节点的时候就移除了监听事件(当使用它的API的时候),这是类库内部处理的,为了防止内存泄露产生。就算在有问题的浏览器下运行,比如…对,IE6 :)
闭包(closure)
javascript 开发一个关键点是 闭包: 一个内部函数可以访问(封闭的)外部函数的变量。由于 javascript 运行环境的实现,以下方式很可能造成内存泄露:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // a reference to 'originalThing'
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
这段代码做了一件事:每次调用 replaceThing
时,theThing
指向了一个包含大数据和闭包(someMethod)的新对象,同时, unused
也指向一个引用了 originalThing
(theThing
指向前一次 replaceThing
调用) 的闭包,已经有点混乱了,啊哈?重点是,一旦为一个具有相同父级作用域的闭包创建作用域,作用域是共享的
这种情况下, 为闭包 someMethod
创建的作用域与 unused
共享。unused
有一个指向 originalThing
的引用。尽管 unused
从未被使用,但是 someMethod
可以通过 theThing
也就是 replaceThing
外围的作用域(例如:全局范围内)调用。与此同时 someMethod
与 unused
共享闭包作用域,unused
引用 originalThing
迫使它保持在内存中(在两个闭包共享的作用域范围)。这阻止了它被回收。
当这段代码反复地运行时,可以观察到内存使用率稳定地增长。GC 运行时也并未减少。本质上,一个闭包链被创建了(通过根作用域的 theThing
变量)。而每一个闭包作用域附带着引用了大数据,结果就是内存泄露。
这个问题被 Meteor 团队发现并发表了著名的文章描述这个问题。
4. 额外的 DOM 引用
有时在数据结构中引用 DOM 节点很有用。假设你想迅速地更新一个表格中的几行内容。可能感觉在字典中或数据中引用 DOM row 是有意义的。在这种情况发生时,两个对相同 DOM 的引用被保持:一个在 DOM 树,另一个在字典中。如果将来某个时间点,你想删除这几行表格,你要确保这两个者失去引用。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// The image is a direct child of the body element.
document.body.removeChild(document.getElementById('image'));
// At this point, we still have a reference to #button in the
//global elements object. In other words, the button element is
//still in memory and cannot be collected by the GC.
}
这里还有额外的点需要注意,当涉及到 DOM 树或子节点的引用。比如说在 javascript 代码中引用了表格的某个格子(一个标签)。某天你决定删除表格但还保持着单元格的引用。直观的假设就是 GC 会回收表格,但保留单元格。事实上,这是不可能的:单元格是表格的子节点,它保持着对父级表格的引用。javascript 代码保持单元格的引用导致 整个表格被保留在内存中。所以请谨慎考虑引用 DOM 元素。