性能調優之Javascript內存泄漏

1.什么是內存泄漏?

內存泄漏是指分配給應用的內存不能被重新分配,即使在內存已經不被使用的時候。正常情況下,垃圾回收器在DOM元素和event處理器不被引用或訪問的時候回收它們。但是,IE的早些版本(IE7和之前)中內存泄漏是很容易出現的,因為內存管理器不能正確理解Javascript生命周期而且在周期被打破(可以通過賦值為null實現)前不會回收內存。

2.為什么你需要注意它?

在大型Web應用程序中內存泄漏是一種常見的意外編程錯誤。內存泄漏會降低Web應用程序的性能,直到浪費的內存超過了系統所能分配的,應用程序將不能使用。作為一Web開發者,開發一個滿足功能要求的應用程序只是第一步,性能要求和Web應用程序的成功是同樣重要的,更何況它可能會導致應用程序錯誤或瀏覽器崩潰。

3.Javascript中出現內存泄漏的主要原因是什么?

1)循環引用

一個很簡單的例子:一個DOM對象被一個Javascript對象引用,與此同時又引用同一個或其它的Javascript對象,這個DOM對象可能會引發內存泄漏。這個DOM對象的引用將不會在腳本停止的時候被垃圾回收器回收。要想破壞循環引用,引用DOM元素的對象或DOM對象的引用需要被賦值為null。

2)Javascript閉包

因為Javascript范圍的限制,許多實現依賴Javascript不包,請查看我的前面的文章JavaScript Scope and Closure如果你想了解更多閉包方面的問題。

閉包可以導致內存泄漏是因為內部方法保持一個對外部方法變量的引用,所以盡管方法返回了內部方法還可以繼續訪問在外部方法中定義的私有變量。對Javascript程序員來說最好的做法是在頁面重載前斷開所有的事件處理器。

3)DOM插入順序

當2個不同范圍的 DOM 對象連添加到一起的時候一個臨時的對象會被創建。這個DOM對象改變范圍到document時,那個臨時對象就沒用了。也就是說, DOM 對象應該按照從當前頁面存在的最上面的 DOM 元素開始往下直到剩下的 DOM 元素的順序添加,這樣它們就總是有同樣的范圍,不會產生臨時對象。

4)如何檢測?

內存泄漏對開發者來說一般很難檢測因為它們是由一些大量代碼中的意外的錯誤引起的,但它在系統內存不足前并不影響程序的功能。這就是為什么會有人在很長時間的測試期中收集應用程序性能指標來測試性能。

最簡單的檢測內存泄漏的方式是用任務管理器檢查內存使用情況。在Chrome瀏覽器的新選項卡中打開應用并查看內存使用量是不是越來越多。還有其他的調試工具提供內存監視器,比如Chrome開發者工具。這是谷歌開者這網站中的堆分析的特性的教程。

什么是內存泄露

內存泄露是指一塊被分配的內存既不能使用,又不能回收,直到瀏覽器進程結束。在C++中,因為是手動管理內存,內存泄露是經常出現的事情。而現在流行的C#和Java等語言采用了自動垃圾回收方法管理內存,正常使用的情況下幾乎不會發生內存泄露。瀏覽器中也是采用自動垃圾回收方法管理內存,但由于瀏覽器垃圾回收方法有bug,會產生內存泄露。

內存泄露Quick View

不同的瀏覽器中存在各種內存泄露方式,目前發現的主要是這樣幾種:

1. 循環引用

已經確認存在泄漏的瀏覽器:IE6.0 FF2.0

含有DOM對象的循環引用將導致大部分當前主流瀏覽器內存泄露 這里有兩個簡單的概念

引用:a.屬性=b,a就引用了b

循環引用:簡單來說假如a引用了b,b又引用了a,a和b就構成了循環引用。

a和b循環引用:

var a=new Object;

var b=new Object;

a.r=b;

b.r=a;

a循環引用自己:

var a=new Object;

a.r=a;

循環引用很常見且大部分情況下是無害的,但當參與循環引用的對象中有DOM對象或者ActiveX對象時,循環引用將導致內存泄露。我們把例子中的任何一個new Object替換成document.getElementById或者document.createElement就會發生內存泄露了。

盡管這看起來非常容易理解,但是因為有closure的參與而使事情變得復雜,有些closure導致的循環引用很難被察覺。下面是一個非常常見的動態綁定事件:

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    obj.onclick=function(){ 
        //Even if it's a empty function 
    } 
}

這個bindEvent執行時100%會發生內存泄露,Someone 可能會問,哪里出現了循環引用? 關于closure和scope chain參與的循環引用比較復雜,此處暫不深入討論。有一個簡單的判斷方式:函數將間接引用所有它能訪問的對象。obj.onclick這個函數中 可以訪問外部的變量obj 所以他引用了obj,而obj又引用了它,因此這個事件綁定將會造成內存泄露。在IBM的文章中介紹了2種方式解決類似的問題一個是obj=null,另一個是把onclick的函數寫在bindEvent外,重復人家的我就不說了。簡單貼下代碼:

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    obj.onclick=onclickHandler; 
} 
function onclickHandler(){ 
    //do something 
}
 
function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    obj.onclick=function(){ 
        //Even if it's a empty function 
    } 
    obj=null; 
}

這兩個方法都打斷了循環引用,可以解決問題,但是似乎對代碼表達能力造成了一定破壞,假設有這么一個問題:

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    var var0="OOXX";//Here is a variable 
    obj.onclick=function(){ 
        alert(var0);//I want to visit var2 here! 
    } 
    return obj;//bindEvent must return obj! 
}

好了,這下兩種辦法都不行了,假如我把函數寫外面去,var0肯定訪問不了,假如我把obj弄成null,還怎么return它呢?這并不是空想的需要,這實際 上是一個用JS定制DOM控件的簡單抽象:創建DOM元素、設置私有屬性、綁定事件。所以,我們必須update一下兩個方法。首先,方法1,為了讓函數 能訪問某些變量,我們可以通過一個Builder函數來訂制onclick的外部閉包:

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    var var0="OOXX";//Here is a variable 
    obj.onclick= onclickBuilder(var0);//想訪問誰就把誰傳進去??! 
    return obj;//bindEvent must return obj! 
} 
function onclickBuilder(var0)//這里跟上面對應上就行了 最好參數名字也對應上 
{ 
    return function(){ 
        alert(var0); 
    } 
}

第二個辦法,這個來自51js的chpn同學,讓obj=null在return 之后執行!!

function bindEvent() 
{ 
    try{ 
        var obj=document.createElement("XXX"); 
        var var0="OOXX";//Here is a variable 
        obj.onclick=function(){ 
            alert(var0);//I want to visit var2 here! 
        } 
        return obj;//bindEvent must return obj! 
    } finally { 
        obj=null; 
    } 
}

2. 某些DOM操作

這是IE系列的特有問題 簡單的來說就是在向不在DOM樹上的DOM元素appendChild,可能會發生內存泄露(只是可能,具體原因不明,似乎十分復雜,下面例子中去掉onClick也可以避免泄露)。所以appendChild的順序可能影響內存泄露,來自微軟的例子:

<html> 
    <head>  
        <script language="JScript"> 
        function LeakMemory()  
        { 
            var hostElement = document.getElementById("hostElement");  
            // Do it a lot, look at Task Manager for memory response  
            for(i = 0; i < 5000; i++)  
            {  
                var parentDiv =  
                    document.createElement("<div onClick='foo()'>");  
                var childDiv = 
                    document.createElement("<div onClick='foo()'>"); 
                // This will leak a temporary object  
                parentDiv.appendChild(childDiv);  
                hostElement.appendChild(parentDiv);  
                hostElement.removeChild(parentDiv); 
                parentDiv.removeChild(childDiv); 
                parentDiv = null; 
                childDiv = null; 
            } 
            hostElement = null; 
        } 
        function CleanMemory() 
        { 
            var hostElement = document.getElementById("hostElement"); 
            // Do it a lot, look at Task Manager for memory response 
            for(i = 0; i < 5000; i++) 
            { 
                var parentDiv = 
                    document.createElement("<div onClick='foo()'>"); 
                var childDiv = 
                    document.createElement("<div onClick='foo()'>"); 
                // Changing the order is important, this won't leak 
                hostElement.appendChild(parentDiv); 
               parentDiv.appendChild(childDiv); 
                hostElement.removeChild(parentDiv); 
                parentDiv.removeChild(childDiv); 
                parentDiv = null; 
                childDiv = null; 
            } 
            hostElement = null; 
        } 
        </script> 
    </head> 
    <body> 
        <button onclick="LeakMemory()">Memory Leaking Insert</button> 
        <button onclick="CleanMemory()">Clean Insert</button> 
        <div id="hostElement"></div> 
    </body> 
</html>

而在IE7中,貌似為了改善內存泄露,IE7采用了極端的解決方案:離開頁面時回收所有DOM樹上的元素,其它一概不管。但是這不僅沒起到任何作用,反而 使問題變得更加復雜。對這類問題,除了自覺一點繞開這些惡心的東西,多用innerHTML這種無用的建議之外。我想可以通過覆蓋 document.createElement來略為改善:

首先我們定義一個看不見的元素當作垃圾箱,所有新創建的元素都扔進垃圾箱里,這樣保證了所有DOM元素都在DOM樹上,IE7就可以正確回收了,另一方面也能避免所謂的”appendChild順序不對導致內存泄露”。

function MemoryFix(){ 
    var garbageBox=document.createElement("div"); 
    garbageBox.style.display="none"; 
    document.body.appendChild(garbageBox); 
    var createElement=document.createElement; 
    document.createElement=function(){ 
        var obj=Function.prototype.apply.apply(createElement,[document,arguments]); 
        garbageBox.appendChild(obj); 
        return obj; 
    } 
}

3. 自動類型裝箱轉換

別不相信,下面的代碼在ie系列中會導致內存泄露

var s=”lalala”;

alert(s.length);

s本身是一個string而非object,它沒有length屬性,所以當訪問length時,JS引擎會自動創建一個臨時String對象封裝s,而這個對象一定會泄露。這個bug匪夷所思,所幸解決起來相當容易,記得所有值類型做.運算之前先顯式轉換一下:

var s="lalala";

alert(new String(s).length);

4) Timers計(定)時器泄露

定時器也是常見產生內存泄露的地方:
for (var i = 0; i < 90000; i++) {
? var buggyObject = {
? ? callAgain: function() {
? ? ? var ref = this;
? ? ? var val = setTimeout(function() {
? ? ? ? ref.callAgain();
? ? ? }, 90000);
? ? }
? }

? buggyObject.callAgain();
? //雖然你想回收但是timer還在
? buggyObject = null;
}