什么是內存泄露
內存泄露是指一塊被分配的內存既不能使用,又不能回收,直到瀏覽器進(jìn)程結束。在C++中,因為是手動(dòng)管理內存,內存泄露是經(jīng)常出現的事情。而現在流行的C#和Java等語(yǔ)言采用了自動(dòng)垃圾回收方法管理內存,正常使用的情況下幾乎不會(huì )發(fā)生內存泄露。瀏覽器中也是采用自動(dòng)垃圾回收方法管理內存,但由于瀏覽器垃圾回收方法有bug,會(huì )產(chǎn)生內存泄露。
內存泄露Quick View
不同的瀏覽器中存在各種內存泄露方式,目前發(fā)現的主要是這樣幾種:
1. 循環(huán)引用
已經(jīng)確認存在泄漏的瀏覽器:IE6.0 FF2.0
含有DOM對象的循環(huán)引用將導致大部分當前主流瀏覽器內存泄露 這里有兩個(gè)簡(jiǎn)單的概念
引用:a.屬性=b,a就引用了b
循環(huán)引用:簡(jiǎn)單來(lái)說(shuō)假如a引用了b,b又引用了a,a和b就構成了循環(huán)引用。
a和b循環(huán)引用:
var a=new Object;
var b=new Object;
a.r=b;
b.r=a;
a循環(huán)引用自己:
循環(huán)引用很常見(jiàn)且大部分情況下是無(wú)害的,但當參與循環(huán)引用的對象中有DOM對象或者ActiveX對象時(shí),循環(huán)引用將導致內存泄露。我們把例子中的任何一個(gè)new Object替換成document.getElementById或者document.createElement就會(huì )發(fā)生內存泄露了。
盡管這看起來(lái)非常容易理解,但是因為有closure的參與而使事情變得復雜,有些closure導致的循環(huán)引用很難被察覺(jué)。下面是一個(gè)非常常見(jiàn)的動(dòng)態(tài)綁定事件:
function bindEvent()
{
var obj=document.createElement("XXX");
obj.onclick=function(){
//Even if it's a empty function
}
}
這個(gè)bindEvent執行時(shí)100%會(huì )發(fā)生內存泄露,Someone 可能會(huì )問(wèn),哪里出現了循環(huán)引用? 關(guān)于closure和scopechain參與的循環(huán)引用比較復雜,此處暫不深入討論。有一個(gè)簡(jiǎn)單的判斷方式:函數將間接引用所有它能訪(fǎng)問(wèn)的對象。obj.onclick這個(gè)函數中可以訪(fǎng)問(wèn)外部的變量obj 所以他引用了obj,而obj又引用了它,因此這個(gè)事件綁定將會(huì )造成內存泄露。在IBM的文章中介紹了2種方式解決類(lèi)似的問(wèn)題一個(gè)是obj=null,另一個(gè)是把onclick的函數寫(xiě)在bindEvent外,重復人家的我就不說(shuō)了。簡(jiǎn)單貼下代碼:
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;
}
這兩個(gè)方法都打斷了循環(huán)引用,可以解決問(wèn)題,但是似乎對代碼表達能力造成了一定破壞,假設有這么一個(gè)問(wèn)題:
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!
}
好了這下兩種辦法都不行了,假如我把函數寫(xiě)外面去,var0肯定訪(fǎng)問(wèn)不了,假如我把obj弄成null,還怎么return它呢?這并不是空想的需要,這實(shí)際上是一個(gè)用JS定制DOM控件的簡(jiǎn)單抽象:創(chuàng )建DOM元素、設置私有屬性、綁定事件。所以,我們必須update一下兩個(gè)方法。首先,方法1,為了讓函數能訪(fǎng)問(wèn)某些變量,我們可以通過(guò)一個(gè)Builder函數來(lái)訂制onclick的外部閉包:
function bindEvent()
{
var obj=document.createElement("XXX");
var var0="OOXX";//Here is a variable
obj.onclick= onclickBuilder(var0);//想訪(fǎng)問(wèn)誰(shuí)就把誰(shuí)傳進(jìn)去??!
return obj;//bindEvent must return obj!
}
function onclickBuilder(var0)//這里跟上面對應上就行了 最好參數名字也對應上
{
return function(){
alert(var0);
}
}
第二個(gè)辦法,這個(gè)來(lái)自51js的chpn同學(xué),讓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系列的特有問(wèn)題 簡(jiǎn)單的來(lái)說(shuō)就是在向不在DOM樹(shù)上的DOM元素appendChild,可能會(huì )發(fā)生內存泄露(只是可能,具體原因不明,似乎十分復雜,下面例子中去掉onClick也可以避免泄露)。所以appendChild的順序可能影響內存泄露,來(lái)自微軟的例子:
</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采用了極端的解決方案:離開(kāi)頁(yè)面時(shí)回收所有DOM樹(shù)上的元素,其它一概不管。但是這不僅沒(méi)起到任何作用,反而使問(wèn)題變得更加復雜。對這類(lèi)問(wèn)題,除了自覺(jué)一點(diǎn)繞開(kāi)這些惡心的東西,多用innerHTML這種無(wú)用的建議之外。我想可以通過(guò)覆蓋document.createElement來(lái)略為改善:
首先我們定義一個(gè)看不見(jiàn)的元素當作垃圾箱,所有新創(chuàng )建的元素都扔進(jìn)垃圾箱里,這樣保證了所有DOM元素都在DOM樹(shù)上,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. 自動(dòng)類(lèi)型裝箱轉換
別不相信,下面的代碼在ie系列中會(huì )導致內存泄露
var s=”lalala”;
alert(s.length);
s本身是一個(gè)string而非object,它沒(méi)有length屬性,所以當訪(fǎng)問(wèn)length時(shí),JS引擎會(huì )自動(dòng)創(chuàng )建一個(gè)臨時(shí)String對象封裝s,而這個(gè)對象一定會(huì )泄露。
這個(gè)bug匪夷所思,所幸解決起來(lái)相當容易,記得所有值類(lèi)型做.運算之前先顯式轉換一下:
var s="lalala";
alert(new String(s).length);
參考
Understanding and Solving Internet Explorer Leak Patterns(中文版)
Memory leak patterns in JavaScript(中文版)
51js的一則討論