1. 程式人生 > >JS哪些操作會造成記憶體洩露

JS哪些操作會造成記憶體洩露

記憶體洩漏:指一塊被分配的記憶體既不能使用,又不能回收,直到瀏覽器程序結束。

1、JS的回收機制

JavaScript垃圾回收的機制很簡單:找出不再使用的變數,然後釋放掉其佔用的記憶體,但是這個過程不是實時的,因為其開銷比較大,所以垃圾回收系統(GC)會按照固定的時間間隔,週期性的執行。

到底哪個變數是沒有用的?所以垃圾收集器必須跟蹤到底哪個變數沒用,對於不再有用的變數打上標記,以備將來收回其記憶體。用於標記的無用變數的策略可能因實現而有所區別,通常情況下有兩種實現方式:標記清除引用計數。引用計數不太常用,標記清除較為常用。

2、標記清除

js中最常用的垃圾回收方式就是標記清除。當變數進入環境時,例如,在函式中宣告一個變數,就將這個變數標記為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變數所佔用的記憶體,因為只要執行流進入相應的環境,就可能會用到它們。而當變數離開環境時,則將其標記為“離開環境”。

function test(){
  var a=10;//被標記,進入環境
  var b=20;//被標記,進入環境
}
test();//執行完畢之後a、b又被標記離開環境,被回收

3、引用此時

引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明瞭一個變數並將一個引用型別值(function object array)賦給該變數時,則這個值的引用次數就是1。如果同一個值又被賦給另一個變數,則該值的引用次數加1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的記憶體空間回收回來。這樣,當垃圾回收器下次再執行時,它就會釋放那些引用次數為0的值所佔用的記憶體。

function test(){
  var a={};//a的引用次數為0
  var b=a;//a的引用次數加1,為1
  var c=a;//a的引用次數加1,為2
  var b={};//a的引用次數減1,為1
}

4、哪些操作會造成記憶體洩露

1)意外的全域性變數引起的記憶體洩露

function leak(){
  leak="xxx";//leak成為一個全域性變數,不會被回收
}

2)閉包引起的記憶體洩露
function bindEvent(){
  var obj=document.createElement("XXX");
  obj.onclick=function(){
    //Even if it's a empty function
  }
}
閉包可以維持函式內區域性變數,使其得不到釋放。 上例定義事件回撥時,由於是函式內定義函式,並且內部函式--事件回撥的引用外暴了,形成了閉包。

解決之道,將事件處理函式定義在外部,解除閉包,或者在定義事件處理函式的外部函式中,刪除對dom的引用。

//將事件處理函式定義在外部
function onclickHandler(){
  //do something
}
function bindEvent(){
  var obj=document.createElement("XXX");
  obj.onclick=onclickHandler;
}

//在定義事件處理函式的外部函式中,刪除對dom的引用
function bindEvent(){
  var obj=document.createElement("XXX");
  obj.onclick=function(){
    //Even if it's a empty function
  }
  obj=null;
}

3)沒有清理的DOM元素引用
var elements={
    button: document.getElementById("button"),
    image: document.getElementById("image"),
    text: document.getElementById("text")
};
function doStuff(){
    image.src="http://some.url/image";
    button.click():
    console.log(text.innerHTML)
}
function removeButton(){
    document.body.removeChild(document.getElementById('button'))
}

4)被遺忘的定時器或者回調
var someResouce=getData();
setInterval(function(){
    var node=document.getElementById('Node');
    if(node){
        node.innerHTML=JSON.stringify(someResouce)
    }
},1000)
這樣的程式碼很常見, 如果 id 為 Node 的元素從 DOM 中移除, 該定時器仍會存在, 同時, 因為回撥函式中包含對 someResource 的引用, 定時器外面的 someResource 也不會被釋放。


5)子元素存在引起的記憶體洩露


黃色是指直接被 js變數所引用,在記憶體裡,紅色是指間接被 js變數所引用,如上圖,refB 被 refA 間接引用,導致即使 refB 變數被清空,也是不會被回收的子元素 refB 由於 parentNode 的間接引用,只要它不被刪除,它所有的父元素(圖中紅色部分)都不會被刪除。

6)IE7/8引用計數使用迴圈引用產生的問題

function fn(){
  var a={};
  var b={};
  a.pro=b;
  b.pro=a;
}
fn();
fn()執行完畢後,兩個物件都已經離開環境,在標記清除方式下是沒有問題的,但是在引用計數策略下,因為a和b的引用次數不為0,所以不會被垃圾回收器回收記憶體,如果fn函式被大量呼叫,就會造成記憶體洩漏。在IE7與IE8上,記憶體直線上升。

IE中有一部分物件並不是原生js物件。例如,其記憶體洩漏DOM和BOM中的物件就是使用C++以COM物件的形式實現的,而COM物件的垃圾回收機制採用的就是引用計數策略。因此,即使IE的js引擎採用標記清除策略來實現,但js訪問的COM物件依然是基於引用計數策略的。換句話說,只要在IE中涉及COM物件,就會存在迴圈引用的問題。

var element=document.getElementById("some_element");
var myObject=new Object();
myObject.e=element;
element.o=myObject;
上面的例子在一個DOM元素(element)與一個原生js物件(myObject)之間建立了迴圈引用。其中,變數myObject有一個名為e的屬性指向element物件;而變數element也有一個屬性名為o回指myObject。由於存在這個迴圈引用,即使例子中的DOM從頁面中移除,它也永遠不會被回收。

看上面的例子,有人會覺得太弱了,誰會做這樣無聊的事情,但是其實我們經常會這樣做

window.onload=function outerFunction(){
  var obj=document.getElementById("element"):
  obj.onclick=function innerFunction(){};
};
這段程式碼看起來沒什麼問題,但是obj引用了document.getElementById(“element”),而document.getElementById(“element”)的onclick方法會引用外部環境中的變數,自然也包括obj,是不是很隱蔽啊。

最簡單的解決方式就是自己手工解除迴圈引用,比如剛才的函式可以這樣

myObject.element=null;
element.o=null;
window.onload=function outerFunction(){
  var obj=document.getElementById("element"):
  obj.onclick=function innerFunction(){};
  obj=null;
};
將變數設定為null意味著切斷變數與它此前引用的值之間的連線。當垃圾回收器下次執行時,就會刪除這些值並回收它們佔用的記憶體。 要注意的是,IE9+並不存在迴圈引用導致Dom記憶體洩漏問題,可能是微軟做了優化,或者Dom的回收方式已經改變

5、如何分析記憶體的使用情況

Google Chrome瀏覽器提供了非常強大的JS除錯工具,Memory 檢視  profiles 檢視讓你可以對 JavaScript 程式碼執行時的記憶體進行快照,並且可以比較這些記憶體快照。它還讓你可以記錄一段時間內的記憶體分配情況。在每一個結果檢視中都可以展示不同型別的列表,但是對我們最有用的是 summary 列表和 comparison 列表。  summary 檢視提供了不同型別的分配物件以及它們的合計大小:shallow size (一個特定型別的所有物件的總和)和 retained size (shallow size 加上保留此物件的其它物件的大小)。distance 顯示了物件到達 GC 根(校者注:最初引用的那塊記憶體,具體內容可自行搜尋該術語)的最短距離。 comparison 檢視提供了同樣的資訊但是允許對比不同的快照。這對於找到洩漏很有幫助。

6、怎樣避免記憶體洩露

1)減少不必要的全域性變數,或者生命週期較長的物件,及時對無用的資料進行垃圾回收;

2)注意程式邏輯,避免“死迴圈”之類的 ;

3)避免建立過多的物件  原則:不用了的東西要及時歸還。