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(){
js= "xxx"; //js成為一個全局變量,不會被回收
}

2)閉包引起的內存泄露

function bindEvent(){
var obj= document.createElement( "XXX");
obj.οnclick= 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.οnclick= 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)子元素存在引起的內存泄露

img

 

黃色是指直接被 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.οnlοad= function outerFunction(){

var obj= document.getElementById( "element"):

obj.οnclick= 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)避免創建過多的對象 原則:不用了的東西要及時歸還。

內存泄漏:指一塊被分配的內存既不能使用,又不能回收,直到瀏覽器進程結束。

 

1、JS的回收機制

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

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

 

2、標記清除

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

 

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


3、引用此時

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

 

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


4、哪些操作會造成內存泄露

1)意外的全局變量引起的內存泄露

 

  1.  
    function leak(){
  2.  
    leak=  "xxx"//leak成為一個全局變量,不會被回收
  3.  
    }


2)閉包引起的內存泄露

 

  1.  
    function bindEvent(){
  2.  
    var obj=  document.createElement(  "XXX");
  3.  
    obj.οnclick=  function(){
  4.  
    //Even if it's a empty function
  5.  
    }
  6.  
    }

閉包可以維持函數內局部變量,使其得不到釋放。 上例定義事件回調時,由於是函數內定義函數,並且內部函數--事件回調的引用外暴了,形成了閉包。

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

  1.  
    //將事件處理函數定義在外部
  2.  
    function onclickHandler(){
  3.  
    //do something
  4.  
    }
  5.  
    function bindEvent(){
  6.  
    var obj=  document.createElement(  "XXX");
  7.  
    obj.onclick=onclickHandler;
  8.  
    }

 

  1.  
    //在定義事件處理函數的外部函數中,刪除對dom的引用
  2.  
    function bindEvent(){
  3.  
    var obj=  document.createElement(  "XXX");
  4.  
    obj.οnclick=  function(){
  5.  
    //Even if it's a empty function
  6.  
    }
  7.  
    obj=  null;
  8.  
    }


3)沒有清理的DOM元素引用

 

  1.  
    var elements={
  2.  
    buttondocument.getElementById(  "button"),
  3.  
    imagedocument.getElementById(  "image"),
  4.  
    textdocument.getElementById(  "text")
  5.  
    };
  6.  
    function doStuff(){
  7.  
    image.src=  "http://some.url/image";
  8.  
    button.click():
  9.  
    console.log(text.innerHTML)
  10.  
    }
  11.  
    function removeButton(){
  12.  
    document.body.removeChild(  document.getElementById(  'button'))
  13.  
    }


4)被遺忘的定時器或者回調

 

  1.  
    var someResouce=getData();
  2.  
    setInterval(  function(){
  3.  
    var node=  document.getElementById(  'Node');
  4.  
    if(node){
  5.  
    node.innerHTML=  JSON.stringify(someResouce)
  6.  
    }
  7.  
    },  1000)

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


5)子元素存在引起的內存泄露

 

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

 

6)IE7/8引用計數使用循環引用產生的問題

 

  1.  
    function fn(){
  2.  
    var a={};
  3.  
    var b={};
  4.  
    a.pro=b;
  5.  
    b.pro=a;
  6.  
    }
  7.  
    fn();

fn()執行完畢后,兩個對象都已經離開環境,在標記清除方式下是沒有問題的,但是在引用計數策略下,因為a和b的引用次數不為0,所以不會被垃圾回收器回收內存,如果fn函數被大量調用,就會造成內存泄漏。在IE7與IE8上,內存直線上升。

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

 

  1.  
    var element=  document.getElementById(  "some_element");
  2.  
    var myObject=  new  Object();
  3.  
    myObject.e=element;
  4.  
    element.o=myObject;

上面的例子在一個DOM元素(element)與一個原生js對象(myObject)之間創建了循環引用。其中,變量myObject有一個名為e的屬性指向element對象;而變量element也有一個屬性名為o回指myObject。由於存在這個循環引用,即使例子中的DOM從頁面中移除,它也永遠不會被回收。

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

 

  1.  
    window.onload=  function outerFunction(){
  2.  
    var obj=  document.getElementById(  "element"):
  3.  
    obj.onclick=  function innerFunction(){};
  4.  
    };

這段代碼看起來沒什么問題,但是obj引用了document.getElementById(“element”),而document.getElementById(“element”)的onclick方法會引用外部環境中的變量,自然也包括obj,是不是很隱蔽啊。

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

  1.  
    myObject.element=  null;
  2.  
    element.o=  null;
  3.  
    window.οnlοad=  function outerFunction(){
  4.  
    var obj=  document.getElementById(  "element"):
  5.  
    obj.οnclick=  function innerFunction(){};
  6.  
    obj=  null;
  7.  
    };

將變量設置為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)避免創建過多的對象  原則:不用了的東西要及時歸還。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM