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
對象的一個屬性仍然保留在內存中,但它不會佔用太多空間。