关于Javascript的内存泄漏问题的整理稿

    技术2022-05-19  23

    今天在做项目遇到javascript的"out of memory"问题,上网查到如下文章,很不错,值得收藏。

    ----------------------------------------------------------------------------------------------------------------------------------------------------

    关于Javascript的内存泄漏问题的整理稿

    作者:tim-wu

    转载自:http://www.blogjava.net/tim-wu/archive/2006/05/29/48729.html

     

    常规循环引用内存泄漏和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.blogjava.net/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://outofhanwell.com/ieleak/index.php?title=Main_Page:有个不错的检测工具

    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 οnlοad='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对象的事件绑定。

    还有一种就是不要那么OO,抛弃Dom的一些特性,用innerHTML代替appendChild,避开循环引用。详细见http://birdshome.cnblogs.com/archive/2005/02/16/104967.html中的讨论贴。

    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。可惜的是,IE不会释放刚才那个临时的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.entryhttp://support.microsoft.com/default.aspx?scid=KB;EN-US;830555http://www.ajaxtopics.com/leakpatterns.htmlhttp://blogs.msdn.com/ericlippert/archive/2003/09/17/53028.aspxhttp://www.quirksmode.org/blog/archives/2005/02/javascript_memo.htmlhttp://youngpup.net/2005/0221010713http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx =http://support.microsoft.com/kb/266071/EN-US ==>IE 5.0至5.5一些版本中的GC bughttp://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.htmlhttp://www.quirksmode.org/dom/innerhtml.htmlhttp://www.crockford.com/javascript/memory/leak.html《JavaScript: The Definitive Guide》4th Editionhttp://outofhanwell.com/ieleak/index.php?title=Main_Page

     


    最新回复(0)