要了解javascript的內(nèi)存泄漏問題,首先要了解的就是javascript的GC原理。
我記得原來在犀牛書《JavaScript: The Definitive
Guide》中看到過,IE使用的GC算法是計(jì)數(shù)器,因此只碰到循環(huán) 引用就會造成memory
leakage。后來一直覺得和觀察到的現(xiàn)象很不一致,直到看到Eric的文章,才明白犀牛書的說法沒有說得很明確,估計(jì)該書成文后IE升級過算法吧
。
在IE 6中,對于javascript object內(nèi)部,jscript使用的是mark-and-sweep算法,而對于javascript
object與外部object(包括native object和vbscript object等等)的引用時,IE 6使用的才是計(jì)數(shù)器的算法。
Eric Lippert在http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx
一文中提到IE 6中JScript的GC算法使用的是nongeneration mark-and-sweep。對于javascript對算法的實(shí)現(xiàn)缺陷,文章如是說:
"The benefits of this approach are numerous, but the principle benefit
is that circular references are not leaked unless the circular
reference involves an object not owned by JScript. "
也就是說,IE 6對于純粹的Script Objects間的Circular
References是可以正確處理的,可惜它處理不了的是JScript與Native Object(例如Dom、ActiveX
Object)之間的Circular References。
所以,當(dāng)我們出現(xiàn)Native對象(例如Dom、ActiveX Object)與Javascript對象間的循環(huán)引用時,內(nèi)存泄露的問題就出現(xiàn)了。當(dāng)然,這個bug在IE 7中已經(jīng)被修復(fù)了
[http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html
]。
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
中有個示意圖和簡單的例子體現(xiàn)了這個問題:
<
html
>
????
<
head
>
????????
<
script?language
=
"
JScript
"
>
????????
var
?myGlobalObject;
????????
function
?SetupLeak()?
//
產(chǎn)生循環(huán)引用,因此會造成內(nèi)存泄露
????????{
????????????
//
?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()?
//
解開循環(huán)引用,解決內(nèi)存泄露問題
????????{
????????????document.getElementById(
"
LeakedDiv
"
).expandoProperty?
=
????????????????
null
;
????????}
????????
</
script
>
????
</
head
>
????
<
body?onload
=
"
SetupLeak()
"
?onunload
=
"
BreakLeak()
"
>
????????
<
div?id
=
"
LeakedDiv
"
></
div
>
????
</
body
>
</
html
>
?? 上面這個例子,看似很簡單就能夠解決內(nèi)存泄露的問題。可惜的是,當(dāng)我們的代碼中的結(jié)構(gòu)復(fù)雜了以后,造成循環(huán)引用的原因開始變得多樣,我們就沒法那么容易觀察到了,這時候,我們必須對代碼進(jìn)行仔細(xì)的檢查。
尤其是當(dāng)碰到Closure,當(dāng)我們往Native對象(例如Dom對象、ActiveX
Object)上綁定事件響應(yīng)代碼時,一個不小心,我們就會制造出Closure Memory
Leak。其關(guān)鍵原因,其實(shí)和前者是一樣的,也是一個跨javascript object和native
object的循環(huán)引用。只是代碼更為隱蔽,這個隱蔽性,是由于javascript的語言特性造成的。但在使用類似內(nèi)嵌函數(shù)的時候,內(nèi)嵌的函數(shù)有擁有一
個reference指向外部函數(shù)的scope,包括外部函數(shù)的參數(shù),因此也就很容易造成一個很隱蔽的循環(huán)引用,例如:
DOM_Node.onevent ->function_object.[ [ scope ] ] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。
[http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
]有個例子極深刻地顯示了該隱蔽性:
<
html
>
????
<
head
>
????????
<
script?language
=
"
JScript
"
>
????????
function
?AttachEvents(element)
????????{
????????????
//
?This?structure?causes?element?to?ref?ClickEventHandler??//element有個引用指向函數(shù)ClickEventHandler()
????????????element.attachEvent(
"
onclick
"
,?ClickEventHandler);
????????????
function
?ClickEventHandler()
????????????{
????????????????
//
?This?closure?refs?element??//該函數(shù)有個引用指向AttachEvents(element)調(diào)用Scope,也就是執(zhí)行了參數(shù)element。
????????????????
????????????}
????????}
????????
function
?SetupLeak()
????????{
????????????
//
?The?leak?happens?all?at?once
????????????AttachEvents(document.getElementById(
"
LeakedDiv
"
));
????????}
????????
</
script
>
????
</
head
>
????
<
body?onload
=
"
SetupLeak()
"
?onunload
=
"
BreakLeak()
"
>
????????
<
div?id
=
"
LeakedDiv
"
></
div
>
????
</
body
>
</
html
>
還有這個例子在IE 6中同樣原因會引起泄露
function
?leakmaybe()?{
var
?elm?
=
?document.createElement(
"
DIV
"
);
??elm.onclick?
=
?
function
()?{
return
?
2
?
+
?
2
;
??}
}
for
?(
var
?i?
=
?
0
;?i??
10000
;?i
++
)?{
??leakmaybe();
}
btw:
關(guān)于Closure的知識,大家可以看看http://jibbering.com/faq/faq_notes/closures.html
這篇文章,習(xí)慣中文也可以看看zkjbeyond的blog,他對Closure這篇文章進(jìn)行了簡要的翻譯:http://m.tkk7.com/zkjbeyond/archive/2006/05/19/47025.html
。
之所以會有這一系列的問題,關(guān)鍵就在于javascript是種函數(shù)式腳本解析語言,因此javascript中“函數(shù)中的變量的作用域是定義作用域,而
不是動態(tài)作用域”,這點(diǎn)在犀牛書《JavaScript: The Definitive Guide》中的“Funtion”一章中有所討論。
http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555
中也對這個問題舉了很詳細(xì)的例子。
目前大多數(shù)ajax前端的javascript framework都利用對事件的管理,解決了該問題。
如果你需要自己解決這個問題,可以參考以下的一些方法:
-
而http://novemberborn.net/javascript/event-cache
一文中則通過增加EventCache,從而給出一個相對結(jié)構(gòu)化的解決方案
/*
????EventCache?Version?1.0
????Copyright?2005?Mark?Wubben
????Provides?a?way?for?automagically?removing?events?from?nodes?and?thus?preventing?memory?leakage.
????See?<http://novemberborn.net/javascript/event-cache>?for?more?information.
????
????This?software?is?licensed?under?the?CC-GNU?LGPL?<http://creativecommons.org/licenses/LGPL/2.1/>
*/
/*
????Implement?array.push?for?browsers?which?don't?support?it?natively.
????Please?remove?this?if?it's?already?in?other?code?
*/
if
(Array.prototype.push?
==
?
null
){
????Array.prototype.push?
=
?
function
(){
????????
for
(
var
?i?
=
?
0
;?i?
<
?arguments.length;?i
++
){
????????????
this
[
this
.length]?
=
?arguments[i];
????????};
????????
return
?
this
.length;
????};
};
/*
????Event?Cache?uses?an?anonymous?function?to?create?a?hidden?scope?chain.
????This?is?to?prevent?scoping?issues.?
*/
var
?EventCache?
=
?
function
(){
????
var
?listEvents?
=
?[];
????
????
return
?{
????????listEvents?:?listEvents,
????
????????add?:?
function
(node,?sEventName,?fHandler,?bCapture){
????????????listEvents.push(arguments);
????????},
????
????????flush?:?
function
(){
????????????
var
?i,?item;
????????????
for
(i?
=
?listEvents.length?
-
?
1
;?i?
>=
?
0
;?i?
=
?i?
-
?
1
){
????????????????item?
=
?listEvents[i];
????????????????
????????????????
if
(item[
0
].removeEventListener){
????????????????????item[
0
].removeEventListener(item[
1
],?item[
2
],?item[
3
]);
????????????????};
????????????????
????????????????
/*
?From?this?point?on?we?need?the?event?names?to?be?prefixed?with?'on"?
*/
????????????????
if
(item[
1
].substring(
0
,?
2
)?
!=
?
"
on
"
){
????????????????????item[
1
]?
=
?
"
on
"
?
+
?item[
1
];
????????????????};
????????????????
????????????????
if
(item[
0
].detachEvent){
????????????????????item[
0
].detachEvent(item[
1
],?item[
2
]);
????????????????};
????????????????
????????????????item[
0
][item[
1
]]?
=
?
null
;
????????????};
????????}
????};
}();
-
使用方法也很簡單:
<script type=
"text/javascript">
function addEvent(oEventTarget, sEventType, fDest){
if(oEventTarget.attachEvent){
oEventTarget.attachEvent("on" + sEventType, fDest);
} elseif(oEventTarget.addEventListener){
oEventTarget.addEventListener(sEventType, fDest, true);
} elseif(typeof oEventTarget[sEventType] == "function"){
var fOld = oEventTarget[sEventType];
oEventTarget[sEventType] = function(e){ fOld(e); fDest(e); };
} else {
oEventTarget[sEventType] = fDest;
};
/* Implementing EventCache for all event systems */
EventCache.add(oEventTarget, sEventType, fDest, true);
};
function createLeak(){
var body = document.body;
function someHandler(){
return body;
};
addEvent(body, "click", someHandler);
};
window.onload = function(){
var i = 500;
while(i > 0){
createLeak();
i = i - 1;
}
};
window.onunload = EventCache.flush;
</script>
-
http://talideon.com/weblog/2005/03/js-memory-leaks.cfm
一文中的方法類似:
/*
?*?EventManager.js
?*?by?Keith?Gaughan
?*
?*?This?allows?event?handlers?to?be?registered?unobtrusively,?and?cleans
?*?them?up?on?unload?to?prevent?memory?leaks.
?*
?*?Copyright?(c)?Keith?Gaughan,?2005.
?*
?*?All?rights?reserved.?This?program?and?the?accompanying?materials
?*?are?made?available?under?the?terms?of?the?Common?Public?License?v1.0
?*?(CPL)?which?accompanies?this?distribution,?and?is?available?at
?*?http://www.opensource.org/licenses/cpl.php
?*
?*?This?software?is?covered?by?a?modified?version?of?the?Common?Public?License
?*?(CPL),?where?Keith?Gaughan?is?the?Agreement?Steward,?and?the?licensing
?*?agreement?is?covered?by?the?laws?of?the?Republic?of?Ireland.
?
*/
//
?For?implementations?that?don't?include?the?push()?methods?for?arrays.
if
?(
!
Array.prototype.push)?{
????Array.prototype.push?
=
?
function
(elem)?{
????????
this
[
this
.length]?
=
?elem;
????}
}
var
?EventManager?
=
?{
????_registry:?
null
,
????Initialise:?
function
()?{
????????
if
?(
this
._registry?
==
?
null
)?{
????????????
this
._registry?
=
?[];
????????????
//
?Register?the?cleanup?handler?on?page?unload.
????????????EventManager.Add(window,?
"
unload
"
,?
this
.CleanUp);
????????}
????},
????
/*
*
?????*?Registers?an?event?and?handler?with?the?manager.
?????*
?????*?@param??obj?????????Object?handler?will?be?attached?to.
?????*?@param??type????????Name?of?event?handler?responds?to.
?????*?@param??fn??????????Handler?function.
?????*?@param??useCapture??Use?event?capture.?False?by?default.
?????*?????????????????????If?you?don't?understand?this,?ignore?it.
?????*
?????*?@return?True?if?handler?registered,?else?false.
?????
*/
????Add:?
function
(obj,?type,?fn,?useCapture)?{
????????
this
.Initialise();
????????
//
?If?a?string?was?passed?in,?it's?an?id.
????????
if
?(
typeof
?obj?
==
?
"
string
"
)?{
????????????obj?
=
?document.getElementById(obj);
????????}
????????
if
?(obj?
==
?
null
?
||
?fn?
==
?
null
)?{
????????????
return
?
false
;
????????}
????????
//
?Mozilla/W3C?listeners?
????????
if
?(obj.addEventListener)?{
????????????obj.addEventListener(type,?fn,?useCapture);
????????????
this
._registry.push({obj:?obj,?type:?type,?fn:?fn,?useCapture:?useCapture});
????????????
return
?
true
;
????????}
????????
//
?IE-style?listeners?
????????
if
?(obj.attachEvent?
&&
?obj.attachEvent(
"
on
"
?
+
?type,?fn))?{
????????????
this
._registry.push({obj:?obj,?type:?type,?fn:?fn,?useCapture:?
false
});
????????????
return
?
true
;
????????}
????????
return
?
false
;
????},
????
/*
*
?????*?Cleans?up?all?the?registered?event?handlers.
?????
*/
????CleanUp:?
function
()?{
????????
for
?(
var
?i?
=
?
0
;?i?
<
?EventManager._registry.length;?i
++
)?{
????????????
with
?(EventManager._registry[i])?{
????????????????
//
?Mozilla/W3C?listeners?
????????????????
if
?(obj.removeEventListener)?{
????????????????????obj.removeEventListener(type,?fn,?useCapture);
????????????????}
????????????????
//
?IE-style?listeners?
????????????????
else
?
if
?(obj.detachEvent)?{
????????????????????obj.detachEvent(
"
on
"
?
+
?type,?fn);
????????????????}
????????????}
????????}
????????
//
?Kill?off?the?registry?itself?to?get?rid?of?the?last?remaining
????????
//
?references.
????????EventManager._registry?
=
?
null
;
????}
};
使用起來也很簡單
<html>
<head>
<script type=text/javascript src=EventManager.js></script>
<script type=text/javascript>
function onLoad() {
EventManager.Add(document.getElementById(testCase),click,hit );
returntrue;
}
function hit(evt) {
alert(click);
}
</script>
</head>
<body onload='javascript: onLoad();'>
<div id='testCase' style='width:100%; height: 100%; background-color: yellow;'>
<h1>Click me!</h1>
</div>
</body>
</html>
-
google map api同樣提供了一個類似的函數(shù)用在頁面的unload事件中,解決Closure帶來的內(nèi)存泄露問題。
??? Cross-Page Leaks和下一節(jié)提到的Pseudo-Leaks在我看來,就是IE的bug, 雖然MS死皮賴臉不承認(rèn):)
???? 大家可以看看這段例子代碼:
<
html
>
????
<
head
>
????????
<
script?language
=
"
JScript
"
>
????????
function
?LeakMemory()?
//
這個函數(shù)會引發(fā)Cross-Page?Leaks
????????{
????????????
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()?
//
而這個函數(shù)不會引發(fā)Cross-Page?Leaks
????????{
????????????
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這兩段函數(shù)的唯一區(qū)別就在于他們的代碼的循序,從代碼上看,兩段代碼的邏輯都沒有錯。
但LeakMemory卻會造成泄露。原因是LeakMemory()會先建立起parentDiv和childDiv之間的連接,這時候,為了讓
childDiv能夠獲知parentDiv的信息,因此IE需要先建立一個臨時的scope對象。而后parentDiv建立了和
hostElement對象的聯(lián)系,parentDiv和childDiv直接使用頁面document的scope。可惜的是,IE不會釋放剛才那個臨
時的scope對象的內(nèi)存空間,直到我們跳轉(zhuǎn)頁面,這塊空間才能被釋放。而CleanMemory函數(shù)不同,他先把parentDiv和
hostElement建立聯(lián)系,而后再把childDiv和parentDiv建立聯(lián)系,這個過程不需要單獨(dú)建立臨時的scope,只要直接使用頁面
document的scope就可以了, 所以也就不會造成內(nèi)存泄露了
詳細(xì)原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
這篇文章。
btw:
IE 6中垃圾回收算法,就是從那些直接"in scope"的對象開始進(jìn)行mark清除的:
Every variable which is "in scope" is called a "scavenger". A scavenger
may refer to a number, an object, a string, whatever. We maintain a
list of scavengers – variables are moved on to the scav list when they
come into scope and off the scav list when they go out of scope.
這個被稱為“秀逗泄露”真是恰當(dāng)啊:)
看看這個例子:
<
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()?{?}
"
;//看內(nèi)存會不斷增加
????????????}
????????}
????????
</
script
>
????
</
head
>
????
<
body
>
????????
<
button?onclick
=
"
LeakMemory()
"
>
Memory?Leaking?Insert
</
button
>
????????
<
script?id
=
"
hostElement
"
>
function
?foo()?{?}
</
script
>
????
</
body
>
</
html
>
MS是這么解釋的,這不是內(nèi)存泄漏。如果您創(chuàng)建了許多無法獲得也無法釋放的對象,那才是內(nèi)存泄漏。在這里,您將創(chuàng)建許多元素,Internet
Explorer 需要保存它們以正確呈現(xiàn)頁面。Internet Explorer
并不知道您以后不會運(yùn)行操縱您剛剛創(chuàng)建的所有這些對象的腳本。當(dāng)頁面消失時(當(dāng)您瀏覽完,離開瀏覽器時)會釋放內(nèi)存。它不會泄漏。當(dāng)銷毀頁面時,會中斷循
環(huán)引用。
唉~~~
詳細(xì)原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
這篇文章。
變量定義一定要用var,否則隱式聲明出來的變量都是全局變量,不是局部變量;
全局變量沒用時記得要置null;
注意正確使用delete,刪除沒用的一些函數(shù)屬性;
注意正確使用try...cache,確保去處無效引用的代碼能被正確執(zhí)行;
open出來的窗口即使close了,它的window對象還是存在的,要記得刪除引用;
frame和iframe的情況和窗口的情況類似。
http://jibbering.com/faq/faq_notes/closures.html
http://javascript.weblogsinc.com/2005/03/07/javascript-memory-leaks/
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
http://72.14.203.104/search?q=cache:V9Bt4_HBzQ8J:jgwebber.blogspot.com/2005/01/dhtml-leaks-like-sieve.html+DHTML+Leaks+Like+a+Sieve+&hl=zh-CN&ct=clnk&cd=9
(這是DHTML Leaks Like a Sieve)一文在google上的cache,原文已經(jīng)連不上了)
http://spaces.msn.com/siteexperts/Blog/cns!1pNcL8JwTfkkjv4gg6LkVCpw!338.entry
http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555
http://www.ajaxtopics.com/leakpatterns.html
http://blogs.msdn.com/ericlippert/archive/2003/09/17/53028.aspx
http://www.quirksmode.org/blog/archives/2005/02/javascript_memo.html
http://youngpup.net/2005/0221010713
http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx
=
http://support.microsoft.com/kb/266071/EN-US
==>IE 5.0至5.5一些版本中的GC bug
http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html
==>ie 7的改進(jìn)
http://erik.eae.net/archives/2006/04/26/23.23.02/
==>ie 7的改進(jìn)
http://www.feedbackarchive.com/spamvampire/today.html
==> Try this script for memory leaks - it leaked 50 megabytes in 15 minutes with firefox on linux:
http://birdshome.cnblogs.com/archive/2005/02/15/104599.html
http://www.quirksmode.org/dom/innerhtml.html
http://www.crockford.com/javascript/memory/leak.html
《JavaScript: The Definitive Guide》4th Edition
http://outofhanwell.com/ieleak/index.php?title=Main_Page