JavaScript内存泄漏
本文翻译自Memory leaks, by Ilya Kantor。
在JavaScript中,我们很少考虑内存管理。我们创建变量,使用它们,并由浏览器去负责处理底层的细节,这看起来似乎挺自然的。
但是随着应用程序变得复杂,以及访客长时间在网页停留,我们可能会注意到一个浏览器需要占1G以上的内存,并且还不断的增长。这通常就是发生了内存泄漏。
在这我们将讨论内存管理和最常见的泄漏类型。
JavaScript的内存管理
JavaScript内存管理的中心思想是一个可达性的思想。
- 假定所有的显著的对象是可达的:这些被称为根。通常,这些包括从调用堆栈中的任何地方引用的所有对象(即,当前正在调用的函数中的所有局部变量和参数)和所有全局对象。
- 对象保存在内存中,而它们可以从根通过引用或引用链访问。
在浏览器中有一个可清除不可达对象占用的内存的垃圾收集器Garbage Collector。
垃圾收集的例子
让我们创建看看它如何工作在下面的代码:
function Menu(title) {
this.title = title;
this.elem = document.getElementById('id');
}
var menu = new Menu('My Menu');
document.body.innerHTML = ''; // (1)
menu = new Menu('His menu'); // (2)
这是内存结构图:
在步骤(1),body.innerHTML
被清空。所以,严格来说,它的孩子应该被删除,因为它们是不可达的。
但是元素#id
是一个例外。它对于menu.elem
是可达的,所以它仍然留着内存中。当然如果你检查它的父节点parentNode
,其值会为null
。
个别的DOM元素可能依然留在内存即使父元素被清除。
在步骤(2),window.menu
引用被重新分配,所以上一个menu
变得不可访问。
它就会被浏览器垃圾收集器自动删除。
现在完整的menu
结构被删除,包括元素。当然,如果有其他部分的代码引用了该元素,那么它会保持不变。
循环引用的收集
闭包往往会导致循环引用。例如:
function setHandler() {
var elem = document.getElementById('id');
elem.onclick = function() {
// ...
};
}
在这里,DOM元素直接通过onclick
属性引用了函数。函数则通过外部的LexicalEnvironment
对象引用了elem
。
即使处理函数内没有代码,也会出现此内存结构。特殊方法addEventListener / attachEvent
同样会产生循环引用。
处理函数通常在elem
元素删除的时候被清除:
function cleanUp() {
var elem = document.getElementById('id');
elem.parentNode.removeChild(elem);
}
调用cleanUp()
函数将elem
元素从DOM移除,elem
仍然有一个引用LexicalEnvironment.elem
,但是没有嵌套函数,所以LexicalEnvironment
变量被回收了。之后,elem
元素变成不可访问并随同它的处理函数一起被清除。
内存泄漏
当浏览器由于某些原因没有释放掉那些不再需要的对象时,便发生了内存泄漏。
这也许是由于浏览器bugs,浏览器拓展问题,或者是我们代码结构中的错误。
IE<8 DOM-JS 内存泄漏
Internet Explorer版本8之前是无法清除DOM对象和JavaScript之间的循环引用。
IE6在SP3(mid-2007 patch)之前,问题更严重,因为内存即使在页面关闭后也不释放。
因此,setHandler
在IE<8下会有泄漏,elem
和闭包从来不会被清理:
function setHandler() {
var elem = document.getElementById('id');
elem.onclick = function() { /* ... */ };
}
除了DOM元素,还有可能是XMLHttpRequest或任何其他COM对象。
IE泄漏的解决方法是打破循环引用。
我们指定elem = null
,这样处理函数不再引用DOM元素,循环关系被打破。
这个泄漏的基本上是有一定历史了,但却是一个很好的打破循环关系的例子。
你可以阅读更多关于它的文章Understanding and Solving Internet Explorer Leak Patterns和 Circular Memory Leak Mitigation
XmlHttpRequest
内存管理和泄漏
下面的代码在IE<9下泄漏:
var xhr = new XMLHttpRequest(); // or ActiveX in older IE
xhr.open('GET', '/server.url', true);
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200) {
// ...
}
};
xhr.send(null);
让我们看看每一步运行的内存结构:
异步XMLHttpRequest
对象是由浏览器追踪的。因此,有一个内部引用。
当请求完成后,引用被删除,所以xhr
变得不可访问。但IE<9却办不到。
幸运的是,修复这个问题是比较容易的。我们需要从闭包中移除xhr
并在处理函数中以this
来访问它:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'jquery.js', true);
xhr.onreadystatechange = function() {
if(this.readyState == 4 && this.status == 200) {
document.getElementById('test').innerHTML++;
}
};
xhr.send(null);
xhr = null;
现在就没有循环引用,泄漏被修复。IE的例子页面。
setInterval/setTimeout
在setTimeout/setInterval
中,函数也会被内部引用并随着其调用完成才被回收。
对于setInterval
,则会在调用clearInterval
时完成并回收。这可能会当函数实际上并没做什么,但定时器又没有被清除,而导致内存泄漏。
对于服务器端的JS和V8,可以看一个问题中的例子: Memory leak when running setInterval in a new context。
内存泄漏的大小
泄漏的数据结构可能不大。
但是闭包使得外部函数的所有变量持续存在当内部函数仍在活动时。
所以想象你创建一个函数,它的一个变量包含一个大字符串。
function f() {
var data = "Large piece of data, probably received from server";
/* do something using data */
function inner() {
// ...
}
return inner;
}
当inner
函数留在内存时,含有一个大变量的LexicalEnvironment
对象将会一直挂在内存中。
JavaScript解释器不知道哪些变量可能是内部函数需要的,所以它在每一个外部
LexicalEnvironment
对象进行完整保存。我希望,新的解释器可以试图优化它,但不确定能否成功。
事实上,有的可能并不是泄漏。许多函数可以被创建是有明确原因的,例如每一个请求,并没有被清除,因为他们是处理函数或其他。
如果变量data
仅用于外部函数,我们可以让它节省内存。
function f() {
var data = "Large piece of data, probably received from server";
/* do something using data */
function inner() {
// ...
}
data = null;
return inner;
}
现在data
作为LexicalEnvironment
对象的一个属性仍然保留在内存中,但它不会占用太多空间。