Js內存泄漏及解決方案

在IE下的JS編程中,如下的編程方式都會形成即便關閉IE也沒法釋放內存的問題,下面分類給出: 

一、給DOM對象添加的屬性是一個對象的引用。範例: 
var MyObject = {}; 
document.getElementById('myDiv').myProp = MyObject; 
解決方法: 
在window.onunload事件中寫上: document.getElementById('myDiv').myProp = null; 


二、DOM對象與JS對象相互引用。範例: 
function Encapsulator(element) { 
this.elementReference = element; 
element.myProp = this; 

new Encapsulator(document.getElementById('myDiv')); 
解決方法: 
在onunload事件中寫上: document.getElementById('myDiv').myProp = null; 


三、給DOM對象用attachEvent綁定事件。範例: 
function doClick() {} 
element.attachEvent("onclick", doClick); 
解決方法: 
在onunload事件中寫上: element.detachEvent('onclick', doClick); 


四、從外到內執行appendChild。這時即便調用removeChild也沒法釋放。範例: 
var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
document.body.appendChild(parentDiv); 
parentDiv.appendChild(childDiv); 
解決方法: 
從內到外執行appendChild: 
var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
parentDiv.appendChild(childDiv); 
document.body.appendChild(parentDiv); 


五、反覆重寫同一個屬性會形成內存大量佔用(但關閉IE後內存會被釋放)。範例: 
for(i = 0; i < 5000; i++) { 
hostElement.text = "asdfasdfasdf"; 

這種方式至關於定義了5000個屬性! 
解決方法: 
其實沒什麼解決方法:P~~~就是編程的時候儘可能避免出現這種狀況咯~~ 


說明: 
一、以上資料均來源於微軟官方的MSDN站點,連接地址: 
http://msdn.microsoft.com/librar ... e_leak_patterns.asp 
你們能夠到上面這個地址中看到詳細的說明,包括範例和圖例都有。只是我英文不太好,看不太懂,若是我上述有失誤或有須要補充的地方請你們指出。 

二、對於第一條,事實上包括 element.onclick = funcRef 這種寫法也算在其中,由於這也是一個對對象的引用。在頁面onunload時應該釋放掉。 

三、對於第三條,在MSDN的英文說明中好像是說即便調用detachEvent也沒法釋放內存,由於在attachEvent的時候就已經形成內存「LEAK」了,不過detachEvent後狀況仍是會好一點。不知道是否是這樣,請英文好的親可以指出。 

四、在實際編程中,這些內存問題的實際影響並不大,尤爲是給客戶使用時,客戶對此毫不會有察覺,然而這些問題對於程序員來講卻始終是個心病 --- 有這樣的BUG內心總會以爲不舒服吧?能解決則給與解決,這樣是最好的。事實上我在webfx.eae.net這樣頂級的JS源碼站點中,在它們的源碼裏都會看到採用上述解決方式進行內存的釋放管理。
html

理解並解決IE的內存泄漏方式 


Web開發的發展 

在過去一些的時候,Web開發人員並無太多的去關注內存泄露問題。那時的頁面間聯繫大都比較簡單,並主要使用不一樣的鏈接地址在同一 

個站點中導航,這樣的設計方式是很是有利於瀏覽器釋放資源的。即便Web頁面運行中真的出現了資源泄漏,那它的影響也是很是有限並且經常 

是不會被人在乎的。 

今天人們對Web應用有了高更的要求。一個頁面極可能數小時不會發生URL跳轉,並同時經過Web服務動態的更新頁面內容。複雜的事件關聯 

設計、基於對象的JScript和DHTML技術的普遍採用,使得代碼的能力達到了其承受的極限。在這樣的狀況和改變下,弄清楚內存泄露方式變得 

很是的急迫,特別是過去這些問題都被傳統的頁面導航方法給屏蔽了。 

還算好的事情是,當你明確了但願尋找什麼時,內存泄露方式是比較容易被肯定的。大多數你能遇到的泄露問題咱們都已經知道,你只需 

要少許額外的工做就會給你帶來好處。雖然在一些頁面中少許的小泄漏問題仍會發生,可是主要的問題仍是很容易解決的。 

泄露方式 

在接下來的內容中,咱們會討論內存泄露方式,併爲每種方式給出示例。其中一個重要的示例是JScript中的Closure技術,另外一個示例是 

在事件執行中使用Closures。當你熟悉本示例後,你就能找出並修改你已有的大多數內存泄漏問題,可是其它Closure相關的問題可能又會被忽 

視。 

如今讓咱們來看看這些個方式都有什麼: 

一、循環引用(Circular References) — IE瀏覽器的COM組件產生的對象實例和網頁腳本引擎產生的對象實例相互引用,就會形成內存泄漏。 

這也是Web頁面中咱們遇到的最多見和主要的泄漏方式; 

二、內部函數引用(Closures) — Closures能夠當作是目前引發大量問題的循環應用的一種特殊形式。因爲依賴指定的關鍵字和語法結構, 

Closures調用是比較容易被咱們發現的; 

三、頁面交叉泄漏(Cross-Page Leaks) — 頁面交叉泄漏實際上是一種較小的泄漏,它一般在你瀏覽過程當中,因爲內部對象薄計引發。下面咱們 

會討論DOM插入順序的問題,在那個示例中你會發現只須要改動少許的代碼,咱們就能夠避免對象薄計對對象構建帶來的影響; 

四、貌似泄漏(Pseudo-Leaks) — 這個不是真正的意義上的泄漏,不過若是你不瞭解它,你可能會在你的可用內存資源變得愈來愈少的時候極 

度鬱悶。爲了演示這個問題,咱們將經過重寫Script元素中的內容來引起大量內存的"泄漏"。 

循環引用 

循環引用基本上是全部泄漏的始做俑者。一般狀況下,腳本引擎經過垃圾收集器(GC)來處理循環引用,可是某些未知因數可能會妨礙從其 

環境中釋放資源。對於IE來講,某些DOM對象實例的狀態是腳本沒法得知的。下面是它們的基本原則: 

    
    Figure 1: 
基本的循環引用模型
本模型中引發的泄漏問題基於COM的引用計數。腳本引擎對象會維持對DOM對象的引用,並在清理和釋放DOM對象指針前等待全部引用的移除 

。在咱們的示例中,咱們的腳本引擎對象上有兩個引用:腳本引擎做用域和DOM對象的expando屬性。當終止腳本引擎時第一個引用會釋放,DOM 

對象引用因爲在等待腳本擎的釋放而並不會被釋放。你可能會認爲檢測並修復假設的這類問題會很是的容易,但事實上這樣基本的的示例只是 

冰山一角。你可能會在30個對象鏈的末尾發生循環引用,這樣的問題排查起來將會是一場噩夢。 

若是你仍不清楚這種泄漏方式在HTML代碼裏到底怎樣,你能夠經過一個全局腳本變量和一個DOM對象來引起並展示它。
程序員


<html> 
<head> 
<script language="JScript"> 
var myGlobalObject; 
function SetupLeak() 

// First set up the script scope to element reference 
myGlobalObject = document.getElementById("LeakedDiv"); 

// Next set up the element to script scope reference 
document.getElementById("LeakedDiv").expandoProperty = myGlobalObject; 


function BreakLeak() 

document.getElementById("LeakedDiv").expandoProperty = null; 

</script> 
</head> 
<body onload="SetupLeak()" onunload="BreakLeak()"> 
<div id="LeakedDiv"></div> 
</body> 
</html>
web

你可使用直接賦null值得方式來破壞該泄漏情形。在頁面文檔卸載前賦null值,將會讓腳本引擎知道對象間的引用鏈沒 

有了。如今它將能正常的清理引用並釋放DOM對象。在這個示例中,做爲Web開發員的你因該更多的瞭解了對象間的關係。 

做爲一個基本的情形,循環引用可能還有更多不一樣的複雜表現。對基於對象的JScript,一個一般用法是經過封裝JScript對象來擴充DOM 

象。在構建過程當中,你經常會把DOM對象的引用放入JScript對象中,同時在DOM對象中也存放上對新近建立的JScript對象的引用。你的這種應 

用模式將很是便於兩個對象之間的相互訪問。這是一個很是直接的循環引用問題,可是因爲使用不用的語法形式可能並不會讓你在乎。要破環 

這種使用情景可能變得更加複雜,固然你一樣可使用簡單的示例以便於清楚的討論。 
<html> 
<head> 
<script language="JScript"> 

function Encapsulator(element) 

// Set up our element 
this.elementReference = element; 

// Make our circular reference 
element.expandoProperty = this; 


function SetupLeak() 

// The leak happens all at once 
new Encapsulator(document.getElementById("LeakedDiv")); 


function BreakLeak() 

document.getElementById("LeakedDiv").expandoProperty = null; 

</script> 
</head> 
<body onload="SetupLeak()" onunload="BreakLeak()"> 
<div id="LeakedDiv"></div> 
</body> 
</html>
算法

更復雜的辦法還有記錄全部須要解除引用的對象和屬性,而後在Web文檔卸載的時候統一清理,但大多數時候你可能會再造 

成額外的泄漏情形,而並無解決你的問題。 

閉包函數(Closures) 
因爲閉包函數會使程序員在不知不覺中建立出循環引用,因此它對資源泄漏經常有着不可推卸的責任。而在閉包函數本身被釋放前,咱們很難判斷父函數的參數以及它的局部變量是否能被釋放。實際上閉包函數的使用已經很普通,以至人們頻繁的遇到這類問題時咱們卻一籌莫展。在詳細瞭解了閉包背後的問題和一些特殊的閉包泄漏示例後,咱們將結合循環引用的圖示找到閉包的所在,並找出這些不受歡迎的引用來至何處。
編程

Figure 2. 閉包函數引發的循環引用瀏覽器

普通的循環引用,是兩個不可探知的對象相互引用形成的,可是閉包卻不一樣。代替直接形成引用,閉包函數則取而代之從其父函數做用域中引入信息。一般,函數的局部變量和參數只能在該被調函數自身的生命週期裏使用。當存在閉包函數後,這些變量和參數的引用會和閉包函數一塊兒存在,但因爲閉包函數能夠超越其父函數的生命週期而存在,因此父函數中的局部變量和參數也仍然能被訪問。在下面的示例中,參數1將在函數調用終止時正常被釋放。當咱們加入了一個閉包函數後,一個額外的引用產生,而且這個引用在閉包函數釋放前都不會被釋放。若是你碰巧將閉包函數放入了事件之中,那麼你不得不手動從那個事件中將其移出。若是你把閉包函數做爲了一個expando屬性,那麼你也須要經過置null將其清除。 

同時閉包會在每次調用中建立,也就是說當你調用包含閉包的函數兩次,你將獲得兩個獨立的閉包,並且每一個閉包都分別擁有對參數的引用。因爲這些顯而易見的因素,閉包確實很是用以帶來泄漏。下面的示例將展現使用閉包的主要泄漏因素: 
<html> 
<head> 
<script language="JScript"> 

function AttachEvents(element) 

// This structure causes element to ref ClickEventHandler 
element.attachEvent("onclick", ClickEventHandler); 

function ClickEventHandler() 

// This closure refs element 



function SetupLeak() 

// The leak happens all at once 
AttachEvents(document.getElementById("LeakedDiv")); 


function BreakLeak() 


</script> 
</head> 
<body onload="SetupLeak()" onunload="BreakLeak()"> 
<div id="LeakedDiv"></div> 
</body> 
</html>
安全

若是你對怎麼避免這類泄漏感到疑惑,我將告訴你處理它並不像處理普通循環引用那麼簡單。"閉包"被看做函數做用域中的一個臨時對象。一旦函數執行退出,你將失去對閉包自己的引用,那麼你將怎樣去調用detachEvent方法來清除引用呢?在Scott Isaacs的MSN Spaces上有一種解決這個問題的有趣方法。這個方法使用一個額外的引用(原文叫second closure,但是這個示例裏致始致終只有一個closure)協助window對象執行onUnload事件,因爲這個額外的引用和閉包的引用存在於同一個對象域中,因而咱們能夠藉助它來釋放事件引用,從而完成引用移除。爲了簡單起見咱們將閉包的引用暫存在一個expando屬性中,下面的示例將向你演示釋放事件引用和清除expando屬性。閉包

<html> 
<head> 
<script language="JScript"> 

function AttachEvents(element) 

// In order to remove this we need to put 
// it somewhere. Creates another ref 
element.expandoClick = ClickEventHandler; 

// This structure causes element to ref ClickEventHandler 
element.attachEvent("onclick", element.expandoClick); 

function ClickEventHandler() 

// This closure refs element 



function SetupLeak() 

// The leak happens all at once 
AttachEvents(document.getElementById("LeakedDiv")); 


function BreakLeak() 

document.getElementById("LeakedDiv").detachEvent("onclick", 
document.getElementById("LeakedDiv").expandoClick); 
document.getElementById("LeakedDiv").expandoClick = null; 

</script> 
</head> 
<body onload="SetupLeak()" onunload="BreakLeak()"> 
<div id="LeakedDiv"></div> 
</body> 
</html>
app

在這篇KB文章中,實際上建議咱們除非無可奈何儘可能不要建立使用閉包。文章中的示例,給咱們演示了非閉包的事件引用方式,即把閉包函數放到頁面的全局做用域中。當閉包函數成爲普通函數後,它將再也不繼承其父函數的參數和局部變量,因此咱們也就不用擔憂基於閉包的循環引用了。在非必要的時候不使用閉包這樣的編程方式能夠儘可能使咱們的代碼避免這樣的問題。 

最後,腳本引擎開發組的Eric Lippert,給咱們帶來了一篇關於閉包使用通俗易懂的好文章。他的最終建議也是但願在真正必要的時候才使用閉包函數。雖然他的文章沒有說起閉包會使用的真正場景,可是這兒已有的大量示例很是有助於你們起步。 

頁面交叉泄漏(Cross-Page Leaks) 
這種基於插入順序而經常引發的泄漏問題,主要是因爲對象建立過程當中的臨時對象未能被及時清理和釋放形成的。它通常在動態建立頁面元素,並將其添加到頁面DOM中時發生。一個最簡單的示例場景是咱們動態建立兩個對象,並建立一個子元素和父元素間的臨時域(譯者注:這裏的域(Scope)應該是指管理元素之間層次結構關係的對象)。而後,當你將這兩個父子結構元素構成的的樹添加到頁面DOM樹中時,這兩個元素將會繼承頁面DOM中的層次管理域對象,並泄漏以前建立的那個臨時域對象。下面的圖示示例了兩種動態建立並添加元素到頁面DOM中的方法。在第一種方法中,咱們將每一個子元素添加到它的直接父元素中,最後再將建立好的整棵子樹添加到頁面DOM中。當一些相關條件合適時,這種方法將會因爲臨時對象問題引發泄漏。在第二種方法中,咱們自頂向下建立動態元素,並使它們被建立後當即加入到頁面DOM結構中去。因爲每一個被加入的元素繼承了頁面DOM中的結構域對象,咱們不須要建立任何的臨時域。這是避免潛在內存泄漏發生的好方法。 
函數

Figure 3. DOM插入順序泄漏模型

接下來,咱們將給出一個躲避了大多數泄漏檢測算法的泄漏示例。由於咱們實際上沒有泄漏任何可見的元素,而且因爲被泄漏的對象過小從而你可能根本不會注意這個問題。爲了使咱們的示例產生泄漏,在動態建立的元素結構中將不得不內聯的包含一個腳本函數指針。在咱們設置好這些元素間的相互隸屬關係後這將會使咱們泄漏內部臨時腳本對象。因爲這個泄漏很小,咱們不得不將示例執行成千上萬次。事實上,一個對象的泄漏只有不多的字節。在運行示例並將瀏覽器導航到一個空白頁面,你將會看到兩個版本代碼在內存使用上的區別。當咱們使用第一種方法,將子元素加入其父元素再將構成的子樹加入頁面DOM,咱們的內存使用量會有微小的上升。這就是一個交叉導航泄漏,只有當咱們從新啓動IE進程這些泄漏的內存纔會被釋放。若是你使用第二種方法將父元素加入頁面DOM再將子元素加入其父元素中,一樣運行若干次後,你的內存使用量將不會再上升,這時你會發現你已經修復了交叉導航泄漏的問題。 
<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>


這類泄漏應該被澄清,由於這個解決方法有悖於咱們在IE中的一些有益經驗。建立帶有腳本對象的DOM元素,以及它們已進行的相互關聯是瞭解這個泄漏的關鍵點。這實際上這對於泄漏來講是相當重要的,由於若是咱們建立的DOM元素不包含任何的腳本對象,同時使用相同的方式將它們進行關聯,咱們是不會有任何泄漏問題的。示例中給出的第二種技巧對於關聯大的子樹結構可能更有效(因爲在那個示例中咱們一共只有兩個元素,因此創建一個和頁面DOM不相關的樹結構並不會有什麼效率問題)。第二個技巧是在建立元素的開始不關聯任何的腳本對象,因此你能夠安全的建立子樹。當你把你的子樹關聯到頁面DOM上後,再繼續處理你須要的腳本事件。牢記並遵照關於循環引用和閉包函數的使用規則,你不會再在掛接事件時在你的代碼中遇到不一樣的泄漏。 

我真的要指出這個問題,由於咱們能夠看出不是全部的內存泄漏都是能夠很容易發現的。它們可能都是些微不足道的問題,但每每須要成千上萬次的執行一個更小的泄漏場景才能使問題顯現出來,就像DOM元素插入順序引發的問題那樣。若是你以爲使用所謂的"最佳"經驗來編程,那麼你就能夠高枕無憂,可是這個示例讓咱們看到,即便是"最佳"經驗彷佛也可能帶來泄漏。咱們這裏的解決方案但願能提升這些已有的好經驗,或者介紹一些新經驗使咱們避免泄漏發生的可能。

貌似泄漏(Pseudo-Leaks) 
在大多數時候,一些APIs的實際的行爲和它們預期的行爲可能會致使你錯誤的判斷內存泄漏。貌似泄漏大多數時候老是出如今同一個頁面的動態腳本操做中,而在從一個頁面跳轉到空白頁面的時候發生是很是少見的。那你怎麼能象排除頁面間泄漏那樣來排除這個問題,而且在新任務運行中的內存使用量是不是你所指望的。咱們將使用腳本文本的重寫來做爲一個貌似泄漏的示例。 

象DOM插入順序問題那樣,這個問題也須要依賴建立臨時對象來產生"泄漏"。對一個腳本元素對象內部的腳本文本一而再再而三的反覆重寫,慢慢地你將開始泄漏各類已關聯到被覆蓋內容中的腳本引擎對象。特別地,和腳本調試有關的對象被做爲徹底的代碼對象形式保留了下來。 
<html> 
<head> 
<script language="JScript"> 
function LeakMemory() 

// Do it a lot, look at Task Manager for memory response 
for(i = 0; i < 5000; i++) 

hostElement.text = "function foo() { }"; 


</script> 
</head> 
<body> 
<button onclick="LeakMemory()">Memory Leaking Insert</button> 
<script id="hostElement">function foo() { }</script> 
</body> 
</html>

若是你運行上面的示例代碼並使用任務管理器查看,當從"泄漏"頁面跳轉到空白頁面時,你並不會注意到任何腳本泄漏。由於這種腳本泄漏徹底發生在頁面內部,並且當你離開該頁面時被使用的內存就會回收。對於咱們本來所指望的行爲來講這樣的狀況是糟糕的。你但願當重寫了腳本內容後,原來的腳本對象就應該完全的從頁面中消失。但事實上,因爲被覆蓋的腳本對象可能已用做事件處理函數,而且還可能有一些未被清除的引用計數。正如你所看到的,這就是貌似泄漏。在表面上內存消耗量可能看起來很是的糟糕,可是這個緣由是徹底能夠接受的。

總結 
每一位Web開發員可能都整理有一份本身的代碼示例列表,當他們在代碼中看到如列表中的代碼時,他們會意識到泄漏的存在並會使用一些開發技巧來避免這些問題。這樣的方法雖然簡單便捷,但這也是今天Web頁面內存泄漏廣泛存在的緣由。考慮咱們所討論的泄漏情景而不是關注獨立的代碼示例,你將會使用更加有效的策略來解決泄漏問題。這樣的觀念將使你在設計階段就把問題估計到,而且確保你有計劃來處理潛在的泄漏問題。使用編寫加固代碼(譯者注:就是異常處理或清理對象等的代碼)的習慣而且採起清理全部本身佔用內存的方法。雖然對這個問題來講可能太誇張了,你也可能幾乎從沒有見到編寫腳本卻須要本身清理本身佔用的內存的狀況;使這個問題變得愈來愈顯著的是,腳本變量和expando屬性間存在的潛在泄漏可能。 

若是對模式和設計感興趣,我強烈推薦Scott的這篇blog,由於其中演示了一個通用的移除基於閉包泄漏的示例代碼。固然這須要咱們使用更多的代碼,可是這個實踐是有效的,而且改進的場景很是容易在代碼中定位並進行調試。相似的注入設計也能夠用在基於expando屬性引發的循環引用中,不過須要注意所註冊的方法自身不要讓泄漏(特別使用閉包的地方)跑掉。

相關文章
相關標籤/搜索