JavaScript內存泄漏

JavaScript內存泄漏

本文翻譯自Memory leaks, by Ilya Kantor

在JavaScript中,我們很少考慮內存管理。我們創建變量,使用它們,並由瀏覽器去負責處理底層的細節,這看起來似乎挺自然的。

但是隨着應用程序變得複雜,以及訪客長時間在網頁停留,我們可能會注意到一個瀏覽器需要佔1G以上的內存,並且還不斷的增長。這通常就是發生了內存泄漏。

在這我們將討論內存管理和最常見的泄漏類型。

JavaScript的內存管理

JavaScript內存管理的中心思想是一個可達性的思想。

  1. 假定所有的顯著的對象是可達的:這些被稱爲根。通常,這些包括從調用堆棧中的任何地方引用的所有對象(即,當前正在調用的函數中的所有局部變量和參數)和所有全局對象。
  2. 對象保存在內存中,而它們可以從根通過引用或引用鏈訪問。

在瀏覽器中有一個可清除不可達對象佔用的內存的垃圾收集器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)

這是內存結構圖:

menu

在步驟(1),body.innerHTML被清空。所以,嚴格來說,它的孩子應該被刪除,因爲它們是不可達的。

但是元素#id是一個例外。它對於menu.elem是可達的,所以它仍然留着內存中。當然如果你檢查它的父節點parentNode,其值會爲null

個別的DOM元素可能依然留在內存即使父元素被清除。

在步驟(2),window.menu引用被重新分配,所以上一個menu變得不可訪問。

它就會被瀏覽器垃圾收集器自動刪除。

menu2

現在完整的menu結構被刪除,包括元素。當然,如果有其他部分的代碼引用了該元素,那麼它會保持不變。

循環引用的收集

閉包往往會導致循環引用。例如:

function setHandler() {

  var elem = document.getElementById('id');

  elem.onclick = function() {
    // ...
  };

}

在這裏,DOM元素直接通過onclick屬性引用了函數。函數則通過外部的LexicalEnvironment對象引用了elem

ie1_1

即使處理函數內沒有代碼,也會出現此內存結構。特殊方法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泄漏的解決方法是打破循環引用。

ie2

我們指定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);

讓我們看看每一步運行的內存結構:

xhr1

異步XMLHttpRequest對象是由瀏覽器追蹤的。因此,有一個內部引用。

當請求完成後,引用被刪除,所以xhr變得不可訪問。但IE<9卻辦不到。

這有一個關於IE的單獨頁面的例子

幸運的是,修復這個問題是比較容易的。我們需要從閉包中移除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;

xhr2

現在就沒有循環引用,泄漏被修復。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對象的一個屬性仍然保留在內存中,但它不會佔用太多空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章