JavaScript 中的內存泄漏

一、是什麼


內存泄漏(Memory leak)是在計算機科學中,由於疏忽或錯誤造成程序未能釋放已經不再使用的內存

並非指內存在物理上的消失,而是應用程序分配某段內存後,由於設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費

程序的運行需要內存。只要程序提出要求,操作系統或者運行時就必須供給內存

對於持續運行的服務進程,必須及時釋放不再用到的內存。否則,內存佔用越來越高,輕則影響系統性能,重則導致進程崩潰

C語言中,因爲是手動管理內存,內存泄露是經常出現的事情。

char * buffer;
buffer = (char*) malloc(42);

// Do something with buffer

free(buffer);

上面是 C 語言代碼,malloc方法用來申請內存,使用完畢之後,必須自己用free方法釋放內存。

這很麻煩,所以大多數語言提供自動內存管理,減輕程序員的負擔,這被稱爲 "垃圾回收機制"

二、垃圾回收機制

Javascript 具有自動垃圾回收機制(GC:Garbage Collecation),也就是說,執行環境會負責管理代碼執行過程中使用的內存

原理:垃圾收集器會定期(週期性)找出那些不在繼續使用的變量,然後釋放其內存

通常情況下有兩種實現方式:

標記清除

JavaScript最常用的垃圾收回機制

當變量進入執行環境是,就標記這個變量爲 “進入環境 “。進入環境的變量所佔用的內存就不能釋放,當變量離開環境時,則將其標記爲 “離開環境 “

垃圾回收程序運行的時候,會標記內存中存儲的所有變量。然後,它會將所有在上下文中的變量,以及被在上下文中的變量引用的變量的標記去掉

在此之後再被加上標記的變量就是待刪除的了,原因是任何在上下文中的變量都訪問不到它們了

隨後垃圾回收程序做一次內存清理,銷燬帶標記的所有值並收回它們的內存

舉個例子:

var m = 0,n = 19 // 把 m,n,add() 標記爲進入環境。
add(m, n) // 把 a, b, c標記爲進入環境。
console.log(n) // a,b,c標記爲離開環境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

引用計數

語言引擎有一張 "引用表",保存了內存裏面所有的資源(通常是各種值)的引用次數。如果一個值的引用次數是0,就表示這個值不再用到了,因此可以將這塊內存釋放

如果一個值不再需要了,引用數卻不爲0,垃圾回收機制無法釋放這塊內存,從而導致內存泄漏

const arr = [1, 2, 3, 4];
console.log('hello world');

面代碼中,數組[1, 2, 3, 4]是一個值,會佔用內存。變量arr是僅有的對這個值的引用,因此引用次數爲1。儘管後面的代碼沒有用到arr,它還是會持續佔用內存

如果需要這塊內存被垃圾回收機制釋放,只需要設置如下:

arr = null

通過設置arrnull,就解除了對數組[1,2,3,4]的引用,引用次數變爲 0,就被垃圾回收了

小結

有了垃圾回收機制,不代表不用關注內存泄露。那些很佔空間的值,一旦不再用到,需要檢查是否還存在對它們的引用。如果是的話,就必須手動解除引用

三、常見內存泄露情況

意外的全局變量

function foo(arg) {
    bar = "this is a hidden global variable";
}

另一種意外的全局變量可能由 this 創建:

function foo() {
    this.variable = "potential accidental global";
}
// foo 調用自己,this 指向了全局對象(window)
foo();

上述使用嚴格模式,可以避免意外的全局變量

定時器也常會造成內存泄露

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 處理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果id爲 Node 的元素從DOM中移除,該定時器仍會存在,同時,因爲回調函數中包含對someResource的引用,定時器外面的someResource也不會被釋放

包括我們之前所說的閉包,維持函數內局部變量,使其得不到釋放

function bindEvent() {
  var obj = document.createElement('XXX');
  var unused = function () {
    console.log(obj, '閉包內引用obj obj不會被釋放');
  };
  obj = null; // 解決方法
}

沒有清理對DOM元素的引用同樣造成內存泄露

const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom刪除了
console.log(refA, 'refA'); // 但是還存在引用能console出整個div 沒有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用

包括使用事件監聽addEventListener監聽的時候,在不監聽的情況下使用removeEventListener取消對事件監聽

參考文獻

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/TEs6JKQsRo2ZbVhVfAuOBA