要了解javascript的內存泄漏問題,首先要了解的就是javascript的GC原理。
我記得原來在犀牛書《JavaScript: The Definitive
Guide》中看到過,IE使用的GC算法是計數器,因此只碰到循環 引用就會造成memory
leakage。后來一直覺得和觀察到的現象很不一致,直到看到Eric的文章,才明白犀牛書的說法沒有說得很明確,估計該書成文后IE升級過算法吧
。
在IE 6中,對于javascript object內部,jscript使用的是mark-and-sweep算法,而對于javascript
object與外部object(包括native object和vbscript object等等)的引用時,IE 6使用的才是計數器的算法。
Eric Lippert在http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx
一文中提到IE 6中JScript的GC算法使用的是nongeneration mark-and-sweep。對于javascript對算法的實現缺陷,文章如是說:
"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。
所以,當我們出現Native對象(例如Dom、ActiveX Object)與Javascript對象間的循環引用時,內存泄露的問題就出現了。當然,這個bug在IE 7中已經被修復了
[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
中有個示意圖和簡單的例子體現了這個問題:
<
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
>
?? 上面這個例子,看似很簡單就能夠解決內存泄露的問題。可惜的是,當我們的代碼中的結構復雜了以后,造成循環引用的原因開始變得多樣,我們就沒法那么容易觀察到了,這時候,我們必須對代碼進行仔細的檢查。
尤其是當碰到Closure,當我們往Native對象(例如Dom對象、ActiveX
Object)上綁定事件響應代碼時,一個不小心,我們就會制造出Closure Memory
Leak。其關鍵原因,其實和前者是一樣的,也是一個跨javascript object和native
object的循環引用。只是代碼更為隱蔽,這個隱蔽性,是由于javascript的語言特性造成的。但在使用類似內嵌函數的時候,內嵌的函數有擁有一
個reference指向外部函數的scope,包括外部函數的參數,因此也就很容易造成一個很隱蔽的循環引用,例如:
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有個引用指向函數ClickEventHandler()
????????????element.attachEvent(
"
onclick
"
,?ClickEventHandler);
????????????
function
?ClickEventHandler()
????????????{
????????????????
//
?This?closure?refs?element??//該函數有個引用指向AttachEvents(element)調用Scope,也就是執行了參數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:
關于Closure的知識,大家可以看看http://jibbering.com/faq/faq_notes/closures.html
這篇文章,習慣中文也可以看看zkjbeyond的blog,他對Closure這篇文章進行了簡要的翻譯:http://www.tkk7.com/zkjbeyond/archive/2006/05/19/47025.html
。
之所以會有這一系列的問題,關鍵就在于javascript是種函數式腳本解析語言,因此javascript中“函數中的變量的作用域是定義作用域,而
不是動態作用域”,這點在犀牛書《JavaScript: The Definitive Guide》中的“Funtion”一章中有所討論。
http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555
中也對這個問題舉了很詳細的例子。
目前大多數ajax前端的javascript framework都利用對事件的管理,解決了該問題。
如果你需要自己解決這個問題,可以參考以下的一些方法:
-
而http://novemberborn.net/javascript/event-cache
一文中則通過增加EventCache,從而給出一個相對結構化的解決方案
/*
????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同樣提供了一個類似的函數用在頁面的unload事件中,解決Closure帶來的內存泄露問題。
??? Cross-Page Leaks和下一節提到的Pseudo-Leaks在我看來,就是IE的bug, 雖然MS死皮賴臉不承認:)
???? 大家可以看看這段例子代碼:
<
html
>
????
<
head
>
????????
<
script?language
=
"
JScript
"
>
????????
function
?LeakMemory()?
//
這個函數會引發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()?
//
而這個函數不會引發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這兩段函數的唯一區別就在于他們的代碼的循序,從代碼上看,兩段代碼的邏輯都沒有錯。
但LeakMemory卻會造成泄露。原因是LeakMemory()會先建立起parentDiv和childDiv之間的連接,這時候,為了讓
childDiv能夠獲知parentDiv的信息,因此IE需要先建立一個臨時的scope對象。而后parentDiv建立了和
hostElement對象的聯系,parentDiv和childDiv直接使用頁面document的scope??上У氖牵琁E不會釋放剛才那個臨
時的scope對象的內存空間,直到我們跳轉頁面,這塊空間才能被釋放。而CleanMemory函數不同,他先把parentDiv和
hostElement建立聯系,而后再把childDiv和parentDiv建立聯系,這個過程不需要單獨建立臨時的scope,只要直接使用頁面
document的scope就可以了, 所以也就不會造成內存泄露了
詳細原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
這篇文章。
btw:
IE 6中垃圾回收算法,就是從那些直接"in scope"的對象開始進行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.
這個被稱為“秀逗泄露”真是恰當啊:)
看看這個例子:
<
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
>
MS是這么解釋的,這不是內存泄漏。如果您創建了許多無法獲得也無法釋放的對象,那才是內存泄漏。在這里,您將創建許多元素,Internet
Explorer 需要保存它們以正確呈現頁面。Internet Explorer
并不知道您以后不會運行操縱您剛剛創建的所有這些對象的腳本。當頁面消失時(當您瀏覽完,離開瀏覽器時)會釋放內存。它不會泄漏。當銷毀頁面時,會中斷循
環引用。
唉~~~
詳細原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
這篇文章。
變量定義一定要用var,否則隱式聲明出來的變量都是全局變量,不是局部變量;
全局變量沒用時記得要置null;
注意正確使用delete,刪除沒用的一些函數屬性;
注意正確使用try...cache,確保去處無效引用的代碼能被正確執行;
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,原文已經連不上了)
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的改進
http://erik.eae.net/archives/2006/04/26/23.23.02/
==>ie 7的改進
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