常規循環引用內存泄漏和Closure內存泄漏

要了解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://youngpup.net/2005/0221010713 中提到:可以利用遞歸Dom樹,解除event綁定,從而解除循環引用:

    														
    if (window.attachEvent) {
    var clearElementProps = [
    'data',
    'onmouseover',
    'onmouseout',
    'onmousedown',
    'onmouseup',
    'ondblclick',
    'onclick',
    'onselectstart',
    'oncontextmenu'
    ];

    window.attachEvent("onunload", function() {
    var el;
    for(var d = document.all.length;d--;){
    el = document.all[d];
    for(var c = clearElementProps.length;c--;){
    el[clearElementProps[c]] = null;
    }
    }
    });
    }
  • 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帶來的內存泄露問題。
  • 當然,如果你不嫌麻煩,你也可以為每個和native object有關的就阿vascript object編寫一個destoryMemory函數,用來手動調用,從而手動解除Dom對象的事件綁定。

Cross-Page Leaks

??? 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.

Pseudo-Leaks

這個被稱為“秀逗泄露”真是恰當啊:)
看看這個例子:

< 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