JavaScript 內存泄露javascript
1、什麼是閉包、以及閉包所涉及的做用域鏈這裏就不說了。 html
2、JavaScript垃圾回收機制 java
JavaScript不須要手動地釋放內存,它使用一種自動垃圾回收機制(garbage collection)。當一個對象無用的時候,即程序中無變量引用這個對象時,就會從內存中釋放掉這個變量。 node
Java代碼 程序員
var s = [ 1, 2 ,3]; web
var s = null; 編程
//這樣原始的數組[1 ,2 ,3]就會被釋放掉了。 數組
var s = [ 1, 2 ,3];瀏覽器
var s = null;閉包
//這樣原始的數組[1 ,2 ,3]就會被釋放掉了。
3、循環引用
三個對象 A 、B 、C
A->B->C :A的某一屬性引用着B,一樣C也被B的屬性引用着。若是將A清除,那麼B、C也被釋放。
A->B->C->B :這裏增長了C的某一屬性引用B對象,若是這是清除A,那麼B、C不會被釋放,由於B和C之間產生了循環引用。Java代碼
var a = {};
a.pro = { a:100 };
a.pro.pro = { b:100 };
a = null ;
//這種狀況下,{a:100}和{b:100}就同時也被釋放了。
var obj = {};
obj.pro = { a : 100 };
obj.pro.pro = { b : 200 };
var two = obj.pro.pro;
obj = null;
//這種狀況下 {b:200}不會被釋放掉,而{a:100}被釋放了。
var a = {};
a.pro = { a:100 };
a.pro.pro = { b:100 };
a = null ;
//這種狀況下,{a:100}和{b:100}就同時也被釋放了。
var obj = {};
obj.pro = { a : 100 };
obj.pro.pro = { b : 200 };
var two = obj.pro.pro;
obj = null;
//這種狀況下 {b:200}不會被釋放掉,而{a:100}被釋放了。
4、循環引用和閉包
Java代碼
function outer(){
var obj = {};
function inner(){
//這裏引用了obj對象
}
obj.inner = inner;
}
function outer(){
var obj = {};
function inner(){
//這裏引用了obj對象
}
obj.inner = inner;
}這是一種及其隱蔽的循環引用,。當調用一次outer時,就會在其內部建立obj和inner兩個對象,obj的inner屬性引用了inner;一樣inner也引用了obj,這是由於obj仍然在innerFun的封閉環境中,準確的講這是因爲JavaScript特有的「做用域鏈」。
所以,閉包很是容易建立循環引用,幸運的是JavaScript可以很好的處理這種循環引用。
5、IE中的內存泄漏
IE中的內存泄漏有好幾種,這裏有詳細的解釋(http://msdn.microsoft.com/en-us/library/bb250448.aspx),園子裏也有翻譯了(http://www.cnblogs.com/birdshome/archive/2006/05/28/ie_memoryleak.html)。
這裏只討論其中一種,即循環引用所形成的內存泄漏,由於,這是一種最廣泛的狀況。
當在DOM元素或一個ActiveX對象與普通JavaScript對象之間存在循環引用時,IE在釋放這類變量時存在特殊的困難,最好手動切斷循環引用,這個bug在IE 7中已經被修復了(http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html)。
「IE 6 suffered from memory leaks when a circular reference between several objects, among which at least one DOM node, was created. This problem has been solved in IE 7. 」
若是上面的例子(第4點)中obj引用的不是一個JavaScript Function對象(inner),而是一個ActiveX對象或Dom元素,這樣在IE中所造成的循環引用沒法獲得釋放。Java代碼
function init(){
var elem = document.getElementByid( 'id' );
elem.onclick = function(){
alert('rain-man');
//這裏引用了elem元素
};
}
function init(){
var elem = document.getElementByid( 'id' );
elem.onclick = function(){
alert('rain-man');
//這裏引用了elem元素
};
}
Elem引用了它的click事件的監聽函數,一樣該函數經過其做用域鏈也引用回了elem元素。這樣在IE中即便離開當前頁面也不會釋放這些循環引用。
6、解決方法
基本的方法就是手動清除這種循環引用,下面一個十分簡單的例子,實際應用時能夠本身構建一個addEvent()函數,而且在window的unload事件上對全部事件綁定進行清除。
Java代碼
function outer(){
var one = document.getElementById( 'one' );
one.onclick = function(){};
}
window.onunload = function(){
var one = document.getElementById( 'one' );
one.onclick = null;
};
JavaScript 中的內存泄漏
JavaScript 是一種垃圾收集式語言,這就是說,內存是根據對象的建立分配給該對象的,並會在沒有對該對象的引用時由瀏覽器收回。JavaScript 的垃圾收集機制自己並無問題,但瀏覽器在爲 DOM 對象分配和恢復內存的方式上卻有些出入。
Internet Explorer 和 Mozilla Firefox 均使用引用計數來爲 DOM 對象處理內存。在引用計數系統,每一個所引用的對象都會保留一個計數,以獲悉有多少對象正在引用它。若是計數爲零,該對象就會被銷燬,其佔用的內存也會返回給堆。雖然這種解決方案總的來講還算有效,但在循環引用方面卻存在一些盲點。
緣由
1)循環引用致使了內存泄漏
<html>
<body>
<script type="text/javascript">
document.write("circular references between JavaScript and DOM!");
var obj;
window.onload = function(){
obj=document.getElementById("DivElement");
document.getElementById("DivElement").expandoProperty=obj;
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
};
</script>
<div id="DivElement">Div Element</div>
</body>
</html>
2)由外部函數調用引發的內存泄漏
<html>
<head>
<script type="text/javascript">
document.write(" object s between JavaScript and DOM!");
function myFunction(element)
{
this.elementReference = element;
// This code forms a circular reference here
//by DOM-->JS-->DOM
element.expandoProperty = this;
}
function Leak() {
//This code will leak
new myFunction(document.getElementById("myDiv"));
}
</script>
</head>
<body onload="Leak()">
<div id="myDiv"></div>
</body>
</html>
3)閉包引發的內存泄漏
function parentFunction(paramA){
var a = paramA;
function childFunction(){
return a + 2;
}
return childFunction();
}
4)由事件處理引發的內存泄漏模式
<html>
<body>
<script type="text/javascript">
document.write("Program to illustrate memory leak via closure");
window.onload=function outerFunction(){
var obj = document.getElementById("element");
obj.onclick=function innerFunction(){
alert("Hi! I will leak");
};
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
// This is used to make the leak significant
};
</script>
<button id="element">Click Me</button>
</body>
</html>
解決方法
1)打破循環引用
<html>
<body>
<script type="text/javascript">
document.write("Avoiding memory leak via closure by breaking the circular reference");
window.onload=function outerFunction(){
var obj = document.getElementById("element");
obj.onclick=function innerFunction()
{
alert("Hi! I have avoided the leak");
// Some logic here
};
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
obj = null; //This breaks the circular reference
};
</script>
<button id="element">"Click Here"</button>
</body>
</html>
2)添加另外一個閉包
<html>
<body>
<script type="text/javascript">
document.write("Avoiding a memory leak by adding another closure");
window.onload=function outerFunction(){
var anotherObj = function innerFunction(){
// Some logic here
alert("Hi! I have avoided the leak");
};
(function anotherInnerFunction(){
var obj = document.getElementById("element");
obj.onclick=anotherObj
})();
};
</script>
<button id="element">"Click Here"</button>
</body>
</html>
3)避免閉包自身
<html>
<head>
<script type="text/javascript">
document.write("Avoid leaks by avoiding closures!");
window.onload=function(){
var obj = document.getElementById("element");
obj.onclick = doesNotLeak;
}
function doesNotLeak(){
//Your Logic here
alert("Hi! I have avoided the leak");
}
</script>
</head>
<body>
<button id="element">"Click Here"</button>
</body>
</html>
4)考慮用CollectGarbage()
jcl.MemFree = function(Mem){
Mem = null;
CollectGarbage();
};
檢測軟件
sIEve: 他是基於ie的內存泄露檢測工具,須要下載運行,http://home.wanadoo.nl/jsrosman/
Leak Monitor: 他是基於firefox的內存泄露檢測工具,https://addons.mozilla.org/firefox/2490/
我的建議
內存回收機制自己有問題,因此開發人員開發的時候儘可能減小內存溢出。不要盲目的追求完美!
JS運行機制
從一個簡單的問題談起:
<script type="text/javascript">
alert(i); // ?
var i = 1;
</script>
輸出結果是undefined, 這種現象被稱成「預解析」:javascript引擎會優先解析var變量和function定義。在預解析完成後,纔會執行代碼。若是一個文檔流中包含多個script代碼段(用script標籤分隔的js代碼或引入的js文件),運行順序是:
step1. 讀入第一個代碼段
step2. 作語法分析,有錯則報語法錯誤(好比括號不匹配等),並跳轉到step5
step3. 對var變量和function定義作「預解析」(永遠不會報錯的,由於只解析正確的聲明)
step4. 執行代碼段,有錯則報錯(好比變量未定義)
step5. 若是還有下一個代碼段,則讀入下一個代碼段,重複step2
step6. 結束
上面的分析,已經能解釋不少問題了,但老以爲欠缺點什麼。好比step3裏,「預解析」到底是怎麼回事?還有step4裏,看下面的例子:
<script type="text/javascript">
alert(i); // error: i is not defined.
i = 1;
</script>
爲何第一句會致使錯誤?javascript中,變量不是能夠不定義嗎?
編譯過程
時間如白馬過隙,書櫃旁翻開恍如隔世般的《編譯原理》,熟悉而又陌生的空白處有着這樣的筆記:
對於傳統編譯型語言來講,編譯步驟分爲:詞法分析、語法分析、語義檢查、代碼優化和字節生成。
但對於解釋型語言來講,經過詞法分析和語法分析獲得語法樹後,就能夠開始解釋執行了。
簡單地說,詞法分析是將字符流(char stream)轉換爲記號流(token stream), 好比將c = a - b;轉換爲:
NAME "c"
EQUALS
NAME "a"
MINUS
NAME "b"
SEMICOLON
上面只是示例,更進一步的瞭解請查看 Lexical Analysis.
《javascript權威指南》的第2章,講的就是詞法結構(Lexical Structure),ECMA-262 中也有描述。詞法結構是一門語言的基礎,很容易掌握。至於詞法分析的實現那是另外一個研究領域,在此不探究。
能夠拿天然語言來類比,詞法分析是一對一的硬性翻譯,好比一段英文,逐詞翻譯成中文,獲得的是一堆記號流,還很難理解。進一步的翻譯,就須要語法分析了,下圖是一個條件語句的語法樹:
構造語法樹的時候,若是發現沒法構造,好比if(a { i = 2; }, 就會報語法錯誤,並結束整個代碼塊的解析,這就是本文開頭部分的step2.
經過語法分析,構造出語法樹後,翻譯出來的句子可能還會有模糊不清的地方,接下來還須要進一步的語義檢查。對於傳統強類型語言來講,語義檢查的主要部分是類型檢查,好比函數的實參和形參類型是否匹配。對於弱類型語言來講,這一步可能沒有(精力有限,沒時間去看JS的引擎實現,不敢肯定JS引擎中是否有語義檢查這一步)。
經過上面的分析能夠看出,對於javascript引擎來講,確定有詞法分析和語法分析,以後可能還有語義檢查、代碼優化等步驟,等這些編譯步驟完成以後(任何語言都有編譯過程,只是解釋型語言沒有編譯成二進制代碼),纔會開始執行代碼。
上面的編譯過程,仍是沒法更深刻的解釋文章開頭部分的「預解析」,咱們還得仔細探究下javascript代碼的執行過程。
執行過程
周愛民在《javascript語言精髓與編程實踐》的第二部分,對此有很是仔細的分析。下面是個人一些領悟:
經過編譯,javascript代碼已經翻譯成了語法樹,而後會馬上按照語法樹執行。
進一步的執行過程,須要理解javascript的做用域機制,javascript採用的是詞法做用域(lexcical scope)。通俗地講,就是javascript變量的做用域是在定義時決定而不是執行時決定,也就是說詞法做用域取決於源碼,編譯器經過靜態分析就能肯定,所以詞法做用域也叫作靜態做用域(static scope)。但須要注意,with和eval的語義沒法僅經過靜態技術實現,實際上,只能說JS的做用域機制很是接近lexical scope.
JS引擎在執行每一個函數實例時,都會建立一個執行環境(execution context)。execution context中包含一個調用對象(call object), 調用對象是一個scriptObject結構,用來保存內部變量表varDecls、內嵌函數表funDecls、父級引用列表upvalue等語法分析結構(注意:varDecls和funDecls等信息是在語法分析階段就已經獲得,並保存在語法樹中。函數實例執行時,會將這些信息從語法樹複製到scriptObject上)。scriptObject是與函數相關的一套靜態系統,與函數實例的生命週期保持一致。
lexical scope是JS的做用域機制,還須要理解它的實現方法,這就是做用域鏈(scope chain)。scope chain是一個name lookup機制,首先在當前執行環境的scriptObject中尋找,沒找到,則順着upvalue到父級scriptObject中尋找,一直lookup到全局調用對象(global object)。
當一個函數實例執行時,會建立或關聯到一個閉包(closure)。 scriptObject用來靜態保存與函數相關的變量表,closure則在執行期動態保存這些變量表及其運行值。closure的生命週期有可能比函數實例長。函數實例在活動引用爲空後會自動銷燬,closure則要等要數據引用爲空後,由JS引擎回收(有些狀況下不會自動回收,就致使了內存泄漏)。
別被上面的一堆名詞嚇住,一旦理解了執行環境、調用對象、閉包、詞法做用域、做用域鏈這些概念,JS語言的不少現象都能迎刃而解。
小結
至此,對於文章開頭部分的疑問,能夠解釋得很清楚了:
step3中所謂的「預解析」,實際上是在step2的語法分析階段完成,並存儲在語法樹中。當執行到函數實例時,會將varDelcs和funcDecls從語法樹中複製到執行環境的scriptObject上。
step4中,未定義變量意味着在scriptObject的變量表中找不到,JS引擎會沿着scriptObject的upvalue往上尋找,若是都沒找到,對於寫操做i = 1; 最後就會等價爲 window.i = 1; 給window對象新增了一個屬性。對於讀操做,若是一直追溯到全局執行環境的scriptObject上都找不到,就會產生運行期錯誤。
JavaScript 內存泄露
今天下午同事讓幫忙看web內存泄露問題。當時定位到建立ActiveX 對象的時候產生的,因而我對這個奇怪的問題進行了一些深刻探索。
不少時候我都依賴javascript的垃圾回收機制,因此對C 以及C++ 操做內存語言常發生的內存泄露是很陌生的。當時建立回調函數用了閉包,固然最終的解決方法是也避免閉包調用。
隨着這個問題的浮出水面,我回憶起之前的一個項目中也應該存在這個內存泄露問題。因而查閱了相關資料把相似的問題總結下來,但願對你們也有幫助。
緣由:對於一門具備垃圾收回機制的語言存在內存泄露,其緣由不外乎就是javascript腳本引擎存在bug。
不少時候,咱們要作的不是去修正那樣的bug,而是想辦法去規避。
目前發現的可能致使內存泄露的代碼有三種:
下面具體的來講說內存是如何泄露的
循環引用:這種方式存在於IE6和FF2中(FF3未作測試),當出現了一個含有DOM對象的循環引用時,就會發生內存泄露。
什麼是循環引用?首先搞清楚什麼是引用,一個對象A的屬性被賦值爲另外一個對象B時,則能夠稱A引用了B。假如B也引用了A,那麼A和B之間構成了循環引用。一樣道理 若是能找到A引用B B引用C C又引用A這樣一組飲用關係,那麼這三個對象構成了循環引用。當一個對象引用本身時,它本身造成了循環引用。注意,在js中變量永遠是對象的屬性,它能夠指向對象,但決不是對象自己。
循環引用很常見,並且一般是無害的,但若是循環引用中包含DOM對象或者ActiveX對象,那麼就會發生內存泄露。例子:
var a=document.createElement("div");
var b=new Object();
a.b=b;
b.a=a;
不少狀況下循環引用不是這樣的明顯,下面就是著名的閉包(closure)形成內存泄露的例子,每執行一次函數A()都會產生內存泄露。試試看,根據前面講的scope對象的知識,能不能找出循環引用?
function A()...{
var a=document.createElement("div");
a.onclick=function()...{
alert("hi");
}
}
A();
OK, 讓咱們來看看。假設A()執行時建立的做用域對象叫作ScopeA 找到如下引用關係
ScopeA引用DOM對象document.createElement("div");
DOM對象document.createElement("div");引用函數function(){alert("hi")}
函數function(){alert("hi")}引用ScopeA
這樣就很清楚了,所謂closure泄露,只不過是幾個js特殊對象的循環引用而已。
自動類型裝箱轉換:這種泄露存在於ie6 ie7中。這是極其匪夷所思的一個bug,看下面代碼
var s="lalalalala";
alert(s.length);
這段代碼怎麼了?看看吧,"lalalalala"已經泄露了。關鍵問題出在s.length上,咱們知道js的類型中,string並不是對象,但能夠對它使用.運算符,爲何呢?由於js的默認類型轉換機制,容許js在遇到.運算符時自動將string轉換爲object型中對應的String對象。而這個轉換成的臨時對象100%會泄露(汗一下)。
某些DOM操做也可能致使泄露 這些噁心的bug只存在於ie系列中。在ie7中 由於試圖fix循環引用bug而讓狀況變得更糟,以致於我對寫這一段種滿了恐懼。
從ie6談起,下面是微軟的例子,
<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>
看看結果吧,LeakMemory形成了內存泄露,而CleanMemory沒有,循環引用了麼?仔細看看沒有。那麼是什麼問題呢?MS的解釋是"插入順序不對",必須先將父級元素appendChild。這聽起來有些模糊,這裏給出一個比較恰當的等價描述:永遠不要使用DOM節點樹以外元素的appendChild方法。
我曾經看到過這樣的說法,建立dom的時候,先建立子節點,當子節點完善後一次性添加到頁面中,不要一點點朝頁面上加東西,儘可能減小document刷新次數,這樣效率會高點。(打個比方就是應該像 LeakMemory )可見這裏我仍是被某些書籍誤導了。至少他沒有告訴我內存泄露的問題。
接下來是ie7和ie8 beta 1中運行這段程序,看到什麼?沒看錯吧,2個都泄露了!別急,刷新一下頁面就行了。爲何呢?ie7改變了DOM元素的回收方式:在離開頁面時回收DOM樹上的全部元素,因此ie7下的內存管理很是簡單:在全部的頁面中只要掛在DOM樹上的元素,就不會泄露,沒掛在DOM樹上,確定泄露。因此,ie7中記住一條原則:在離開頁面以前把全部建立的DOM元素掛到DOM樹上。
接下來談談ie7的這個設計吧,坦白的說,這種作法純粹是偷懶的垃圾作法。動態垃圾回收不是保證全部內存都在離開頁面時收回,而是要保證內存的充分利用,運行時不回收,等到離開時回收有什麼用?這只是名義上的避免泄露,實際上是徹底的泄露。何況尚未回收DOM節點樹以外的元素。
4.內存泄露的解決方案
內存泄露怎麼辦?真的之後不用閉包了麼?無法封裝控件了?這樣作還不如要了js程序員的命,嘿嘿。
事實上,經過一些很簡單的小技巧,能夠巧妙的繞開這些危險的bug。
to be continued......
coming soon:
首先說說最容易處理的狀況 對於類型轉換形成的錯誤,咱們能夠經過顯式類型轉換來避免:
var s=newString("lalalalala");//此處將string轉換成object
alert(s.length);
這個太容易了,算不上正經方案。不過類型轉換泄露也就這一種處理方法了。
在比較成熟的js程序員裏,把事件函數寫成閉包是再正常不過了:
function A(){
var a=document.createElement("div");
a.onclick=function(){
alert("hi");
}
}
這將致使內存泄露。按照IBM那兩位老大的說法,固然是把函數放外面或者a=null就沒問題了,不過還要訪問A()裏面的變量呢?假若有下面的代碼:
function A(){
var a=document.createElement("div");
var b=document.createElement("div");
a.onclick=function(){
alert(b.outerHTML);
}
return a;
}
如何將它的邏輯表達出來 還避免內存泄露? 分析一下這個內存泄露的形式:只要onclick的外部環境中不包含a那麼,就不會泄露。那麼辦法有2個一是將環境到a的引用斷開 另外一個是將function到環境的引用斷開,可是,若是要在函數中訪問b就不能將Function放到外面,若是要返回a的值,就不能a=null,怎麼辦呢?
解決方案1:
構造一個不含a的新環境
function A(){
var a=document.createElement("div");
var b=document.createElement("div");
a.onclick=BuildEvent(b);
return a;
}
function BuildEvent(b)
{
return function(){
alert(b.outerHTML);
}
}
a自己能夠經過this訪問,將其它須要訪問的外層函數變量傳遞給BuildEvent就能夠了。保持BuildEvent定義和調用的參數名一致,會帶來方便。
解決方案2:
在return 以後a=null,不可能? 看看下面:
function A(){
try{
var a=document.createElement("div");
var b=document.createElement("div");
a.onclick= function(){
alert(b.outerHTML);
}
return a;
} finally {
a=null;
}
}
finally在try以後執行,若是finall塊不返回值,纔會返回try塊的返回值。
還記得函數的lazy initalize吧,對於ie噁心至極的DOM操做泄露,咱們須要用相似的方法去處理。在一個函數中構造一個複雜對象,在須要的時候將之appendChild到DOM樹上,這是很常見的作法,但在IE6中,這樣作將致使所謂的"插入順序內存泄露",沒有別的辦法,咱們只能用一個數組parts保存子節點,編寫一個appendTo方法先序遍歷節點樹,去把它掛在某個DOM節點上。
function appendTo(Element)
...{
Element.appendChild(this);
if(!this.parts)return;
for(var i=0;i<this.parts.length;i++)
parts.appendTo(this);
}
對於ie7,我比較迫不得已,由於DOM對象不會被CG程序回收,只有離開頁面時會被回收,因此個人建議是:使用DOM要有節制,儘可能多用innerHTML吧...... good luck.
一旦你使用了DOM對象,千萬不要試圖o=null,你能夠設置一個叫作Garbage的div而且將其display設置爲none,將不用的DOM對象存入其中(就是appendChild上去)就行了
這是Ext的作法,這裏只是順帶提一下。將每一個元素用一個"代理對象"操做,不論appendChild仍是其餘操做都不是對DOM對象自己的操做,而是經過這個代理對象操做。這是一個很不錯的Proxy模式,不過要想避免泄露仍是須要一點功夫的,並不是用了Proxy以後就不會泄露,有時反而更容易泄露。
5 .FAQ
1 內存泄露是內存佔用很大麼? 不是,即便1byte內存也叫作內存泄露。
2 程序中提示,內存不足,是內存泄露麼?不是,這通常是無限遞歸函數調用致使棧內存溢出。
3 內存泄露是哪一個區域泄露?堆區,棧區是不會泄露的。
4 window對象是DOM對象麼?不是,window對象參與的循環引用不會內存泄露。
5 內存泄露後果是什麼?大多數時候後果不很嚴重,但過多DOM操做會致使網頁執行變慢。
6 跳轉頁面後,內存泄露仍然存在麼?仍然存在,直到關閉瀏覽器。
7 FireFox也會內存泄露麼?FF2仍然有內存泄露
個人瀏覽器存在泄漏麼?
Internet Explorer 和 Mozilla Firefox 是兩個與 JavaScript 中的內存泄漏聯繫最爲緊密的瀏覽器。兩個瀏覽器中形成這種問題的「罪魁禍首」是用來管理 DOM 對象的組件對象模型。本機 Windows COM 和 Mozilla's XPCOM 都使用引用計數的垃圾收集來進行內存分配和檢索。引用計數與用於 JavaScript 的標記-清除式的垃圾收集並不老是能相互兼容。本文側重介紹的是如何應對 JavaScript 代碼中的內存泄漏。有關如何處理 Firefox 和 IE 中 COM 層內存泄漏的更多信息,請參看 參考資料。
JavaScript 中的內存泄露模式
JavaScript 是用來向 Web 頁面添加動態內容的一種功能強大的腳本語言。它尤爲特別有助於一些平常任務,好比驗證密碼和建立動態菜單組件。JavaScript 易學易用,但卻很容易在某些瀏覽器中引發內存的泄漏。在這個介紹性的文章中,咱們解釋了 JavaScript 中的泄漏由何引發,展現了常見的內存泄漏模式,並介紹瞭如何應對它們。
注意本文假設您已經很是熟悉使用 JavaScript 和 DOM 元素來開發 Web 應用程序。本文尤爲適合使用 JavaScript 進行 Web 應用程序開發的開發人員,也可供有興趣建立 Web 應用程序的客戶提供瀏覽器支持以及負責瀏覽器故障排除的人員參考。
JavaScript 中的內存泄漏
JavaScript 是一種垃圾收集式語言,這就是說,內存是根據對象的建立分配給該對象的,並會在沒有對該對象的引用時由瀏覽器收回。JavaScript 的垃圾收集機制自己並無問題,但瀏覽器在爲 DOM 對象分配和恢復內存的方式上卻有些出入。
Internet Explorer 和 Mozilla Firefox 均使用引用計數來爲 DOM 對象處理內存。在引用計數系統,每一個所引用的對象都會保留一個計數,以獲悉有多少對象正在引用它。若是計數爲零,該對象就會被銷燬,其佔用的內存也會返回給堆。雖然這種解決方案總的來講還算有效,但在循環引用方面卻存在一些盲點。
循環引用的問題何在?
當兩個對象互相引用時,就構成了循環引用,其中每一個對象的引用計數值都被賦 1。在純垃圾收集系統中,循環引用問題不大:若涉及到的兩個對象中的一個對象被任何其餘對象引用,那麼這兩個對象都將被垃圾收集。而在引用計數系統,這兩個對象都不能被銷燬,緣由是引用計數永遠不能爲零。在同時使用了垃圾收集和引用計數的混合系統中,將會發生泄漏,由於系統不能正確識別循環引用。在這種狀況下,DOM 對象和 JavaScript 對象均不能被銷燬。清單 1 顯示了在 JavaScript 對象和 DOM 對象間存在的一個循環引用。
清單 1. 循環引用致使了內存泄漏
<html> <body> <script type="text/javascript"> document.write("circular references between JavaScript and DOM!"); var obj; window.onload = function(){ obj=document.getElementById("DivElement"); document.getElementById("DivElement").expandoProperty=obj; obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX")); }; </script> <div id="DivElement">Div Element</div> </body> </html>
如上述清單中所示,JavaScript 對象 obj 擁有到 DOM 對象的引用,表示爲 DivElement。而 DOM 對象則有到此 JavaScript 對象的引用,由 expandoProperty 表示。可見,JavaScript 對象和 DOM 對象間就產生了一個循環引用。因爲 DOM 對象是經過引用計數管理的,因此兩個對象將都不能銷燬。
另外一種內存泄漏模式
在清單 2 中,經過調用外部函數 myFunction 建立循環引用。一樣,JavaScript 對象和 DOM 對象間的循環引用也會致使內存泄漏。
清單 2. 由外部函數調用引發的內存泄漏
<html> <head> <script type="text/javascript"> document.write(" object s between JavaScript and DOM!"); function myFunction(element) { this.elementReference = element; // This code forms a circular reference here //by DOM-->JS-->DOM element.expandoProperty = this; } function Leak() { //This code will leak new myFunction(document.getElementById("myDiv")); } </script> </head> <body onload="Leak()"> <div id="myDiv"></div> </body> </html>
正如這兩個代碼示例所示,循環引用很容易建立。在 JavaScript 最爲方便的編程結構之一:閉包中,循環引用尤爲突出。
JavaScript 中的閉包
JavaScript 的過人之處在於它容許函數嵌套。一個嵌套的內部函數能夠繼承外部函數的參數和變量,並由該外部函數私有。清單 3 顯示了內部函數的一個示例。
清單 3. 一個內部函數
function parentFunction(paramA) { var a = paramA; function childFunction() { return a + 2; } return childFunction(); }
JavaScript 開發人員使用內部函數來在其餘函數中集成小型的實用函數。如清單 3 所示,此內部函數 childFunction 能夠訪問外部函數 parentFunction 的變量。當內部函數得到和使用其外部函數的變量時,就稱其爲一個閉包。
瞭解閉包
考慮如清單 4 所示的代碼片斷。
清單 4. 一個簡單的閉包
<html> <body> <script type="text/javascript"> document.write("Closure Demo!!"); window.onload= function closureDemoParentFunction(paramA) { var a = paramA; return function closureDemoInnerFunction (paramB) { alert( a +" "+ paramB); }; }; var x = closureDemoParentFunction("outer x"); x("inner x"); </script> </body> </html>
在上述清單中,closureDemoInnerFunction 是在父函數 closureDemoParentFunction 中定義的內部函數。當用外部的 x 對 closureDemoParentFunction 進行調用時,外部函數變量 a 就會被賦值爲外部的 x。函數會返回指向內部函數 closureDemoInnerFunction 的指針,該指針包括在變量 x 內。
外部函數 closureDemoParentFunction 的本地變量 a 即便在外部函數返回時仍會存在。這一點不一樣於 C/C++ 這樣的編程語言,在 C/C++ 中,一旦函數返回,本地變量也將不復存在。在 JavaScript 中,在調用 closureDemoParentFunction 的時候,帶有屬性 a 的範圍對象將會被建立。該屬性包括值 paramA,又稱爲「外部 x」。一樣地,當 closureDemoParentFunction 返回時,它將會返回內部函數 closureDemoInnerFunction,該函數包括在變量 x 中。
因爲內部函數持有到外部函數的變量的引用,因此這個帶屬性 a 的範圍對象將不會被垃圾收集。當對具備參數值 inner x 的 x 進行調用時,即 x("inner x"),將會彈出警告消息,代表 「outer x innerx」。
清單 4 簡要解釋了 JavaScript 閉包。閉包功能很是強大,緣由是它們使內部函數在外部函數返回時也仍然能夠保留對此外部函數的變量的訪問。不幸的是,閉包很是易於隱藏 JavaScript 對象 和 DOM 對象間的循環引用。
閉包和循環引用
在清單 5 中,能夠看到一個閉包,在此閉包內,JavaScript 對象(obj)包含到 DOM 對象的引用(經過 id "element" 被引用)。而 DOM 元素則擁有到 JavaScript obj 的引用。這樣創建起來的 JavaScript 對象和 DOM 對象間的循環引用將會致使內存泄漏。
清單 5. 由事件處理引發的內存泄漏模式
<html> <body> <script type="text/javascript"> document.write("Program to illustrate memory leak via closure"); window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction(){ alert("Hi! I will leak"); }; obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX")); // This is used to make the leak significant }; </script> <button id="element">Click Me</button> </body> </html>
避免內存泄漏
幸虧,JavaScript 中的內存泄漏是能夠避免的。當肯定了可致使循環引用的模式以後,正如咱們在上述章節中所作的那樣,您就能夠開始着手應對這些模式了。這裏,咱們將以上述的 由事件處理引發的內存泄漏模式 爲例來展現三種應對已知內存泄漏的方式。
一種應對 清單 5 中的內存泄漏的解決方案是讓此 JavaScript 對象 obj 爲空,這會顯式地打破此循環引用,如清單 6 所示。
清單 6. 打破循環引用
<html> <body> <script type="text/javascript"> document.write("Avoiding memory leak via closure by breaking the circular reference"); window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction() { alert("Hi! I have avoided the leak"); // Some logic here }; obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX")); obj = null; //This breaks the circular reference }; </script> <button id="element">"Click Here"</button> </body> </html>
清單 7 是經過添加另外一個閉包來避免 JavaScript 對象和 DOM 對象間的循環引用。
清單 7. 添加另外一個閉包
<html> <body> <script type="text/javascript"> document.write("Avoiding a memory leak by adding another closure"); window.onload=function outerFunction(){ var anotherObj = function innerFunction() { // Some logic here alert("Hi! I have avoided the leak"); }; (function anotherInnerFunction(){ var obj = document.getElementById("element"); obj.onclick=anotherObj })(); }; </script> <button id="element">"Click Here"</button> </body> </html>
清單 8 則經過添加另外一個函數來避免閉包自己,進而阻止了泄漏。
清單 8. 避免閉包自身
<html> <head> <script type="text/javascript"> document.write("Avoid leaks by avoiding closures!"); window.onload=function() { var obj = document.getElementById("element"); obj.onclick = doesNotLeak; } function doesNotLeak() { //Your Logic here alert("Hi! I have avoided the leak"); } </script> </head> <body> <button id="element">"Click Here"</button> </body> </html>