关于Javascript的内存泄漏问题

常规循环引用内存泄漏和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();
}

关于Closure的知识,大家可以看看 这篇文章 ,习惯中文也可以看看zkjbeyond的blog,他对Closure 这篇文章进行了 简要的翻译 。之所以会有这一系列的问题,关键就在于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);
                 }
                 else if(obj.detachEvent){//  IE-style listeners?
                    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);
   return true;   
}   
function hit(evt) {       
   alert(click);   
}
</script>
</head>
<body > 
<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">
// 这个函数会引发Cross-Page Leaks
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 >");
        var  childDiv = document.createElement("<div >");
        //  This will leak a temporary object
        parentDiv.appendChild(childDiv);
        hostElement.appendChild(parentDiv);
        hostElement.removeChild(parentDiv);
        parentDiv.removeChild(childDiv);
        parentDiv = null ;
        childDiv = null ;
     }
     hostElement = null ;
}
// 而这个函数不会引发Cross-Page Leaks
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 >");
         var childDiv = document.createElement("<div >");
         //  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 > Memory Leaking Insert </button>
<button > 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这篇文章。

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

posted @ 2008-07-08 15:25  广陵散仙(www.cnblogs.com/junzhongxu/)  阅读(300)  评论(0编辑  收藏  举报