理解并解决IE的内存泄漏方式

    这篇文章其实已经看了有些日子了,并且最近的一些开发都在尽量的遵循文中的原则。可是目前的情况是代码规模稍微大点以后,IE的内存泄漏还是很严重,于是我非常生气(倒没啥后果)觉得该把这篇文章挖出来批批。为了方便批斗,所以决定先给翻译成中文,结果在精读以后,发现每个泄漏情景的描述和避免,作者几乎都留了一手,所以这么看来文章又都对了,没啥可批的啦。只是让我想起啦真的刘一手。。。


Author: Justin Rogers ,Micrsoft Corporation June 2005
Translator by: http://birdshome.cnblogs.com

Web开发的发展

    在过去一些的时候,Web开发人员并没有太多的去关注内存泄露问题。那时的页面间联系大都比较简单,并主要使用不同的连接地址在同一个站点中导航,这样的设计方式是非常有利于浏览器释放资源的。即使Web页面运行中真的出现了资源泄漏,那它的影响也是非常有限而且常常是不会被人在意的。

    今天人们对Web应用有了高更的要求。一个页面很可能数小时不会发生URL跳转,并同时通过Web服务动态的更新页面内容。复杂的事件关联设计、基于对象的JScript和DHTML技术的广泛采用,使得代码的能力达到了其承受的极限。在这样的情况和改变下,弄清楚内存泄露方式变得非常的急迫,特别是过去这些问题都被传统的页面导航方法给屏蔽了。

    还算好的事情是,当你明确了希望寻找什么时,内存泄露方式是比较容易被确定的。大多数你能遇到的泄露问题我们都已经知道,你只需要少量额外的工作就会给你带来好处。虽然在一些页面中少量的小泄漏问题仍会发生,但是主要的问题还是很容易解决的。

泄露方式

    在接下来的内容中,我们会讨论内存泄露方式,并为每种方式给出示例。其中一个重要的示例是JScript中的Closure技术,另一个示例是在事件执行中使用Closures。当你熟悉本示例后,你就能找出并修改你已有的大多数内存泄漏问题,但是其它Closure相关的问题可能又会被忽视。

现在让我们来看看这些个方式都有什么:

 1、循环引用(Circular References) — IE浏览器的COM组件产生的对象实例和网页脚本引擎产生的对象实例相互引用,就会造成内存泄漏。这也是Web页面中我们遇到的最常见和主要的泄漏方式;

 2、内部函数引用(Closures) — Closures可以看成是目前引起大量问题的循环应用的一种特殊形式。由于依赖指定的关键字和语法结构,Closures调用是比较容易被我们发现的;

 3、页面交叉泄漏(Cross-Page Leaks) — 页面交叉泄漏其实是一种较小的泄漏,它通常在你浏览过程中,由于内部对象薄计引起。下面我们会讨论DOM插入顺序的问题,在那个示例中你会发现只需要改动少量的代码,我们就可以避免对象薄计对对象构建带来的影响;

 4、貌似泄漏(Pseudo-Leaks) — 这个不是真正的意义上的泄漏,不过如果你不了解它,你可能会在你的可用内存资源变得越来越少的时候极度郁闷。为了演示这个问题,我们将通过重写Script元素中的内容来引发大量内存的"泄漏"。

循环引用

    循环引用基本上是所有泄漏的始作俑者。通常情况下,脚本引擎通过垃圾收集器(GC)来处理循环引用,但是某些未知因数可能会妨碍从其环境中释放资源。对于IE来说,某些DOM对象实例的状态是脚本无法得知的。下面是它们的基本原则:

    CircularReferences.gif
    Figure 1: 基本的循环引用模型

    本模型中引起的泄漏问题基于COM的引用计数。脚本引擎对象会维持对DOM对象的引用,并在清理和释放DOM对象指针前等待所有引用的移除。在我们的示例中,我们的脚本引擎对象上有两个引用:脚本引擎作用域和DOM对象的expando属性。当终止脚本引擎时第一个引用会释放,DOM对象引用由于在等待脚本擎的释放而并不会被释放。你可能会认为检测并修复假设的这类问题会非常的容易,但事实上这样基本的的示例只是冰山一角。你可能会在30个对象链的末尾发生循环引用,这样的问题排查起来将会是一场噩梦。

    如果你仍不清楚这种泄漏方式在HTML代码里到底怎样,你可以通过一个全局脚本变量和一个DOM对象来引发并展现它。

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

    你可以使用直接赋null值得方式来破坏该泄漏情形。在页面文档卸载前赋null值,将会让脚本引擎知道对象间的引用链没有了。现在它将能正常的清理引用并释放DOM对象。在这个示例中,作为Web开发员的你因该更多的了解了对象间的关系。

    作为一个基本的情形,循环引用可能还有更多不同的复杂表现。对基于对象的JScript,一个通常用法是通过封装JScript对象来扩充DOM对象。在构建过程中,你常常会把DOM对象的引用放入JScript对象中,同时在DOM对象中也存放上对新近创建的JScript对象的引用。你的这种应用模式将非常便于两个对象之间的相互访问。这是一个非常直接的循环引用问题,但是由于使用不用的语法形式可能并不会让你在意。要破环这种使用情景可能变得更加复杂,当然你同样可以使用简单的示例以便于清楚的讨论。

< html >
    
< head >
        
< script  language ="JScript" >

        
function Encapsulator(element)
        
{
            
// Set up our element
            this.elementReference = element;

            
// Make our circular reference
            element.expandoProperty = this;
        }


        
function SetupLeak()
        
{
            
// The leak happens all at once
            new Encapsulator(document.getElementById("LeakedDiv"));
        }


        
function BreakLeak()
        
{
            document.getElementById(
"LeakedDiv").expandoProperty = null;
        }

        
</ script >
    
</ head >
    
< body  onload ="SetupLeak()"  onunload ="BreakLeak()" >
        
< div  id ="LeakedDiv" ></ div >
    
</ body >
</ html >

    更复杂的办法还有记录所有需要解除引用的对象和属性,然后在Web文档卸载的时候统一清理,但大多数时候你可能会再造成额外的泄漏情形,而并没有解决你的问题。

    to be continued ...

    // Closure我没有翻,他的表现是内部函数,并且可以访问父函数的变量,有人翻成闭包被骂啦一头包

===========================================================================================

   大家节日快乐!俺就继续这个IE内存泄漏的主题来作为节日礼物了,并且相当欢迎大家来一起讨论。这一节讲Closures引起的内存泄漏,最后我还是决定把Closures翻译成了闭包或闭包函数。而且又在KB中看到一个对Closures的解释,它是这么说的:

< HTML >
< HEAD >
< script  language ="javascript" >
function initpage()
{
    window.setTimeout(
"window.location.reload()"500"javascript");
}

</ script >
</ HEAD >
< body  onload ="initpage()"   >
< div  class ='menu'  id ='menu' ></ div >
< script  language ='javascript' >
hookup(document.getElementById('menu'));
function hookup(element)
{
    element.attachEvent( 
"onmouseover", mouse);
    
function mouse () 
    
{
    }

}

</ script >
</ body >
</ HTML >

    In this code, the handler (the mouse function) is nested inside the attacher (the hookup function). This arrangement means that the handler is closed over the scope of the caller (this arrangement is named a "closure").


闭包函数(Closures)

    由于闭包函数会使程序员在不知不觉中创建出循环引用,所以它对资源泄漏常常有着不可推卸的责任。而在闭包函数自己被释放前,我们很难判断父函数的参数以及它的局部变量是否能被释放。实际上闭包函数的使用已经很普通,以致人们频繁的遇到这类问题时我们却束手无策。在详细了解了闭包背后的问题和一些特殊的闭包泄漏示例后,我们将结合循环引用的图示找到闭包的所在,并找出这些不受欢迎的引用来至何处。

    CircularReferences.gif
    Figure 2. 闭包函数引起的循环引用

    普通的循环引用,是两个不可探知的对象相互引用造成的,但是闭包却不同。代替直接造成引用,闭包函数则取而代之从其父函数作用域中引入信息。通常,函数的局部变量和参数只能在该被调函数自身的生命周期里使用。当存在闭包函数后,这些变量和参数的引用会和闭包函数一起存在,但由于闭包函数可以超越其父函数的生命周期而存在,所以父函数中的局部变量和参数也仍然能被访问。在下面的示例中,参数1将在函数调用终止时正常被释放。当我们加入了一个闭包函数后,一个额外的引用产生,并且这个引用在闭包函数释放前都不会被释放。如果你碰巧将闭包函数放入了事件之中,那么你不得不手动从那个事件中将其移出。如果你把闭包函数作为了一个expando属性,那么你也需要通过置null将其清除。

    同时闭包会在每次调用中创建,也就是说当你调用包含闭包的函数两次,你将得到两个独立的闭包,而且每个闭包都分别拥有对参数的引用。由于这些显而易见的因素,闭包确实非常用以带来泄漏。下面的示例将展示使用闭包的主要泄漏因素:

< html >
    
< head >
        
< script  language ="JScript" >

        
function AttachEvents(element)
        
{
            
// This structure causes element to ref ClickEventHandler
            element.attachEvent("onclick", ClickEventHandler);

            
function ClickEventHandler()
            
{
                
// This closure refs element
            }

        }


        
function SetupLeak()
        
{
            
// The leak happens all at once
            AttachEvents(document.getElementById("LeakedDiv"));
        }


        
function BreakLeak()
        
{
        }

        
</ script >
    
</ head >
    
< body  onload ="SetupLeak()"  onunload ="BreakLeak()" >
        
< div  id ="LeakedDiv" ></ div >
    
</ body >
</ html >


    如果你对怎么避免这类泄漏感到疑惑,我将告诉你处理它并不像处理普通循环引用那么简单。"闭包"被看作函数作用域中的一个临时对象。一旦函数执行退出,你将失去对闭包本身的引用,那么你将怎样去调用detachEvent方法来清除引用呢?在Scott Isaacs的MSN Spaces上有一种解决这个问题的有趣方法。这个方法使用一个额外的引用(原文叫second closure,可是这个示例里致始致终只有一个closure)协助window对象执行onUnload事件,由于这个额外的引用和闭包的引用存在于同一个对象域中,于是我们可以借助它来释放事件引用,从而完成引用移除。为了简单起见我们将闭包的引用暂存在一个expando属性中,下面的示例将向你演示释放事件引用和清除expando属性。

< html >
    
< head >
        
< script  language ="JScript" >

        
function AttachEvents(element)
        
{
            
// In order to remove this we need to put
            // it somewhere. Creates another ref
            element.expandoClick = ClickEventHandler;

            
// This structure causes element to ref ClickEventHandler
            element.attachEvent("onclick", element.expandoClick);

            
function ClickEventHandler()
            
{
                
// This closure refs element
            }

        }


        
function SetupLeak()
        
{
            
// The leak happens all at once
            AttachEvents(document.getElementById("LeakedDiv"));
        }


        
function BreakLeak()
        
{
            document.getElementById(
"LeakedDiv").detachEvent("onclick",
                document.getElementById(
"LeakedDiv").expandoClick);
            document.getElementById(
"LeakedDiv").expandoClick = null;
        }

        
</ script >
    
</ head >
    
< body  onload ="SetupLeak()"  onunload ="BreakLeak()" >
        
< div  id ="LeakedDiv" ></ div >
    
</ body >
</ html >


    在这篇KB文章中,实际上建议我们除非迫不得已尽量不要创建使用闭包。文章中的示例,给我们演示了非闭包的事件引用方式,即把闭包函数放到页面的全局作用域中。当闭包函数成为普通函数后,它将不再继承其父函数的参数和局部变量,所以我们也就不用担心基于闭包的循环引用了。在非必要的时候不使用闭包这样的编程方式可以尽量使我们的代码避免这样的问题。

    最后,脚本引擎开发组的Eric Lippert,给我们带来了一篇关于闭包使用通俗易懂的好文章。他的最终建议也是希望在真正必要的时候才使用闭包函数。虽然他的文章没有提及闭包会使用的真正场景,但是这儿已有的大量示例非常有助于大家起步。

    to be continued ...


    不得不说Eric Lippert同志疾呼的:Don't use closures unless you really need closure semantics. In most cases, non-nested functions are the right way to go. 是非常消极的应付之辞。今天关于IE内存泄漏的文章已有很多,而且很大部分就是微软的自己人在解释,甚至象Eric Lippert这样引擎开发组的成员。但是他们的文章始终没有正面承认其实这就是IE的bug,而且是非常严重的bug,这事情其实完全不关脚本引擎对象和DOM对象的事。就是微软对产品不负责任的表现,不说IE4,1997那个春天那是太遥远了点,但是IE6也是2001年随xp发布的。使用COM可以给他们的开发带来很多便利,当然也利用很多现成的东西,可是居然在带来这样的严重问题后,他们却把大部分责任归咎于不合理和不正确的使用Closures技术!对于循环引用产生Memory Leak我根本就是不好意思提了,那样的问题是应该发生的吗?那就让我们拭目以待看IE7怎么解决这堆shit问题吧,当然我希望不要再看到类似:Do not use closures unless they are necessary. 这样的扯淡建议!

#9楼    回复  引用    

其实不用对闭包那么恐惧.....
实际上闭包的问题和对象的问题本质上是一样的
甚至可以把闭包看作特殊的“对象”

再看下面的例子:
function A()
{
var a = document.getElementById("some_ele");
return function(){alert(1);}
}
var a = A();
有人认为这种闭包一定会导致内存泄漏
及时你返会的闭包中没有引用a,但是因为闭包的缘故导致a不能被销毁

然而这种说法严格来说不完全准确
实际上上面这种用法对内存的耗费情况和下面
function A()
{
var a = document.getElementById("some_ele");
}
var a = new A()

完全一样
前者是因为调用对象不能被销毁
而后者是因为实际对象不能被销毁,它们的本质完全相同

a = null;
CollectBarbage();
都能释放它们

===========================================================================================

    这一节非常的让人郁闷,不管是翻译过程还是文章内容本身。居然对DOM的这样简单操作区别都可以带来这么大的泄漏问题,真的是没有语言了。看完后欢迎说说你的感受,我的感受附在了文尾。



页面交叉泄漏(Cross-Page Leaks)

    这种基于插入顺序而常常引起的泄漏问题,主要是由于对象创建过程中的临时对象未能被及时清理和释放造成的。它一般在动态创建页面元素,并将其添加到页面DOM中时发生。一个最简单的示例场景是我们动态创建两个对象,并创建一个子元素和父元素间的临时域(译者注:这里的域(Scope)应该是指管理元素之间层次结构关系的对象)。然后,当你将这两个父子结构元素构成的的树添加到页面DOM树中时,这两个元素将会继承页面DOM中的层次管理域对象,并泄漏之前创建的那个临时域对象。下面的图示示例了两种动态创建并添加元素到页面DOM中的方法。在第一种方法中,我们将每个子元素添加到它的直接父元素中,最后再将创建好的整棵子树添加到页面DOM中。当一些相关条件合适时,这种方法将会由于临时对象问题引起泄漏。在第二种方法中,我们自顶向下创建动态元素,并使它们被创建后立即加入到页面DOM结构中去。由于每个被加入的元素继承了页面DOM中的结构域对象,我们不需要创建任何的临时域。这是避免潜在内存泄漏发生的好方法。

    DOMInsertionOrder.gif
    Figure 3. DOM插入顺序泄漏模型

    接下来,我们将给出一个躲避了大多数泄漏检测算法的泄漏示例。因为我们实际上没有泄漏任何可见的元素,并且由于被泄漏的对象太小从而你可能根本不会注意这个问题。为了使我们的示例产生泄漏,在动态创建的元素结构中将不得不内联的包含一个脚本函数指针。在我们设置好这些元素间的相互隶属关系后这将会使我们泄漏内部临时脚本对象。由于这个泄漏很小,我们不得不将示例执行成千上万次。事实上,一个对象的泄漏只有很少的字节。在运行示例并将浏览器导航到一个空白页面,你将会看到两个版本代码在内存使用上的区别。当我们使用第一种方法,将子元素加入其父元素再将构成的子树加入页面DOM,我们的内存使用量会有微小的上升。这就是一个交叉导航泄漏,只有当我们重新启动IE进程这些泄漏的内存才会被释放。如果你使用第二种方法将父元素加入页面DOM再将子元素加入其父元素中,同样运行若干次后,你的内存使用量将不会再上升,这时你会发现你已经修复了交叉导航泄漏的问题。

< html >
    
< head >
        
< script  language ="JScript" >

        
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 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()
        
{
            
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 >

    这类泄漏应该被澄清,因为这个解决方法有悖于我们在IE中的一些有益经验。创建带有脚本对象的DOM元素,以及它们已进行的相互关联是了解这个泄漏的关键点。这实际上这对于泄漏来说是至关重要的,因为如果我们创建的DOM元素不包含任何的脚本对象,同时使用相同的方式将它们进行关联,我们是不会有任何泄漏问题的。示例中给出的第二种技巧对于关联大的子树结构可能更有效(由于在那个示例中我们一共只有两个元素,所以建立一个和页面DOM不相关的树结构并不会有什么效率问题)。第二个技巧是在创建元素的开始不关联任何的脚本对象,所以你可以安全的创建子树。当你把你的子树关联到页面DOM上后,再继续处理你需要的脚本事件。牢记并遵守关于循环引用和闭包函数的使用规则,你不会再在挂接事件时在你的代码中遇到不同的泄漏。

    我真的要指出这个问题,因为我们可以看出不是所有的内存泄漏都是可以很容易发现的。它们可能都是些微不足道的问题,但往往需要成千上万次的执行一个更小的泄漏场景才能使问题显现出来,就像DOM元素插入顺序引起的问题那样。如果你觉得使用所谓的"最佳"经验来编程,那么你就可以高枕无忧,但是这个示例让我们看到,即使是"最佳"经验似乎也可能带来泄漏。我们这里的解决方案希望能提高这些已有的好经验,或者介绍一些新经验使我们避免泄漏发生的可能。

    to be continued ...



    不得不承认仔细的弄完这节的内容后,我有点被搞糊涂了。这个所谓的什么Cross-Page Leaks,是目前IE所有内存泄漏中我认为最严重的,前面两节说道的循环引用闭包函数都是我们可以发现并合理避免的。这里先不说Cross-Page Leaks到底有多少种场景,单示例给出的这个DOM插入顺序问题就够搞死人了。因为在使用Dhtml时,使用一个GetXxx、CreateXxx或GenerateXxx函数来得到一个复杂的Html对象结构,再attach到需要的primary DOM上是再平常不过的操作了,现在连这样都要带来Leak,真的是印证了这年头连萝卜都靠不住。之前看到过一个说法,由于IE的一些窗口之间有所有(owner)关系,比如谁是谁的openner啥的,所以IE会保持一些navigate away的页面内容,以便于被别的页面引用。可是不知道IE怎么搞得,连页面跳转到了别的domain或完全的空白(about:blank)页中后,所占用的内存还是不能释放。这样严重的bug,微软还能美其名曰Cross-Page Leaks,真的是绝倒。其实只要能保证navigate to other domain或navigate to blank page后完全释放内存(这里根本都不需要GC,直接全部release就行了),其它的泄漏问题也就没有那么尖锐了,因为这时用户在不关闭IE的情况下总还有个完全清理并回收内存的机会。那种运行了很久后,IE占用2,3百M内存(PM+VM)的情况就能大大的减小。不能回收内存的另一个莫名其妙的问题是,这时脚本的引擎的效率会变得异常的低。话又说回来,现在的机器,都是512M或1G的内存,IE就是占用2,3百M内存,甚至3,4百M也不是什么不能忍受的问题。可是这个时候,脚本引擎不知道怎么搞得,运行效率变得非常的低,似乎是每次GC脚本引擎都要把那些已leak的内存完全的sweep and mark一遍,那个代价可想而知。当然这是一个猜想,否则也无法解释为什么脚本引擎的效率变得如此的低。
===========================================================================================

#11楼    回复  引用    

哈哈,发现这篇文章本身的bug了!
之所以LeakMemory()会产生泄漏,并非文章中的原因!!!
关键在于:createElement("<div οnclick='foo()'>") 中的onclick上!!

改成createElement('div')就没有泄漏啦!(IE6 sp2测试过)
然后再加上parentDiv.οnclick=LeakMemory这个事件绑定也没有泄漏


所以根本原因极有可能是IE对createElement('<div οnclick="foo">')的处理有bug,在形成临时域时,内部对onclick事件的js函数循环引用了
解决办法就非常简单了,不要createElement使用带事件绑定的元素,应该在create之后用代码来绑定

#14楼    回复  引用    

呵呵,再细看文章,其中也提到了这个泄漏的是onclick的脚本对象,关注文章强调的创建顺序,把脚本对象的字眼给漏掉了,呵呵...
总之很容易解决,可以松口气了

 

===========================================================================================

 

   这段时间挺忙,不过还好终于圆满提交了 产品的一个重要feature, Enterprise Overall Dashboard,有空再介绍一下。关于这个IE内存泄漏虽然是越来越郁闷,不过郁闷归郁闷,还是因该要有个善始善终,于是把最后一节翻完了放上来,这也好是个完整的系列。

貌似泄漏(Pseudo-Leaks)

    在大多数时候,一些APIs的实际的行为和它们预期的行为可能会导致你错误的判断内存泄漏。貌似泄漏大多数时候总是出现在同一个页面的动态脚本操作中,而在从一个页面跳转到空白页面的时候发生是非常少见的。那你怎么能象排除页面间泄漏那样来排除这个问题,并且在新任务运行中的内存使用量是否是你所期望的。我们将使用脚本文本的重写来作为一个貌似泄漏的示例。

    象DOM插入顺序问题那样,这个问题也需要依赖创建临时对象来产生"泄漏"。对一个脚本元素对象内部的脚本文本一而再再而三的反复重写,慢慢地你将开始泄漏各种已关联到被覆盖内容中的脚本引擎对象。特别地,和脚本调试有关的对象被作为完全的代码对象形式保留了下来。

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


    如果你运行上面的示例代码并使用任务管理器查看,当从"泄漏"页面跳转到空白页面时,你并不会注意到任何脚本泄漏。因为这种脚本泄漏完全发生在页面内部,而且当你离开该页面时被使用的内存就会回收。对于我们原本所期望的行为来说这样的情况是糟糕的。你希望当重写了脚本内容后,原来的脚本对象就应该彻底的从页面中消失。但事实上,由于被覆盖的脚本对象可能已用作事件处理函数,并且还可能有一些未被清除的引用计数。正如你所看到的,这就是貌似泄漏。在表面上内存消耗量可能看起来非常的糟糕,但是这个原因是完全可以接受的。

总结

    每一位Web开发员可能都整理有一份自己的代码示例列表,当他们在代码中看到如列表中的代码时,他们会意识到泄漏的存在并会使用一些开发技巧来避免这些问题。这样的方法虽然简单便捷,但这也是今天Web页面内存泄漏普遍存在的原因。考虑我们所讨论的泄漏情景而不是关注独立的代码示例,你将会使用更加有效的策略来解决泄漏问题。这样的观念将使你在设计阶段就把问题估计到,并且确保你有计划来处理潜在的泄漏问题。使用编写加固代码(译者注:就是异常处理或清理对象等的代码)的习惯并且采取清理所有自己占用内存的方法。虽然对这个问题来说可能太夸张了,你也可能几乎从没有见到编写脚本却需要自己清理自己占用的内存的情况;使这个问题变得越来越显著的是,脚本变量和expando属性间存在的潜在泄漏可能。

    如果对模式和设计感兴趣,我强烈推荐Scott的这篇blog,因为其中演示了一个通用的移除基于闭包泄漏的示例代码。当然这需要我们使用更多的代码,但是这个实践是有效的,并且改进的场景非常容易在代码中定位并进行调试。类似的注入设计也可以用在基于expando属性引起的循环引用中,不过需要注意所注册的方法自身不要让泄漏(特别使用闭包的地方)跑掉。

About the author 

    Justin Rogers recently joined the Internet Explorer team as an Object Model developer working on extensibility and previously worked on such notable projects as the .NET QuickStart Tutorials, .NET Terrarium, and SQL Reporting Services Management Studio in SQL Server 2005.

[全文完] 注意: 本翻译未经作者授权,任何转载责任自负。

本系列文章:

 

===========================================================================================

    我在前段时间介绍过IE中JavaScript脚本 Memory Leak的问题,后来在几位热心网友的讨论下,基本认可了内存泄露的 事实和原理。在小规模的测试case下,本来都达到了基本避免IE中脚本的ML问题。可是近来发现只以"仔细"来防止IE中脚本ML似乎是非常困难的一件事情,难道开始的讨论有错误吗?

    何谓"仔细"呢?就是说在有对象相互引用的时候,在对象丢弃时(不一定是页面refresh)断开彼此的引用链,特别是脚本中创建的对象和DHTML中的对象间的引用;清除HTML元素中的所有自定义属性;清除所有HTML元素中的事件处理函数回调;对数组在废弃时尽力delete掉内部元素。

    最重要的就是,尽量不创建冗余的脚本对象和DHTML元素对象,能通过修改属性来达到的效果,即使麻烦一些也不重新生成新的对象。

    通过上面的步骤后,IE的内存使用增长率有所下降。可是仍然不能完全满足对复杂的脚本运行的支持(接近Bindows这种复杂程度),体现在以下几点:
    一、在脚本执行过程中,内存使用量仍然是个只增不减的过程;
    二、使用最小化IE窗口方式强制IE进行GC,只能GC物理内存,对虚拟内存无效;
    三、页面跳离(URL改变)原脚本执行域,内存释放量太少甚至不释放;
    四、必须关闭IEXPLORE.EXE进程(即所有IE窗口),才能完全释放IE所使用的内存。

    今天突然想起来久违的 Bindows,跑去一看,2月底release了一个1.3版本,于是开始运行主页上面给的demo。效果不用说了,自己去看一下就行了,效率也相当的高。demo里还有一个类似多维数据显示的GRID,居然还支持行和列的表头都固定。炫已经是bindows亘古不变的特点了,在还没有被迷昏前,我想起应该看看Bindows对内存的处理怎么样?真是不看不知道,一看吓一跳!

    打开 www.bindows.net,我的IE内存使用量在(28PM+18VM)M左右,打开它的 demo program。内存上到(38PM+35VM)M左右,然后再操作了几下,内存到了(80PM+75VM)M左右。于是关掉demo窗口,IE释放了大概15M左右内存,就停在(70PM+70VM)M的水平,在改变当前IE的URL,跳到了google,IE的内存使用量似乎还是没有减少@_@。哈哈,Bindows也有Memory Leak~。真是小人得志,555... 过了一段短时间再看,IE的内存使用降到和开启IE时差不多了:)。真实好消息,看来不能再冤枉IE了,于是开始跟踪Bindows在onunload时的处理代码。

    怎么能一下就跳到onunload的代码里去呢?这里有个hack,先对IE按下Alt+V,u,b(需要uncheck IE options高级中的"禁止脚本调试",菜单View里才有U快捷键选项)。然后立即关闭Bindows的演示dome窗口,选择VS.NET 2003作为Script调试器,就直接跳到onunload的入口处了。

    在管理IE中的脚本内存使用中,Bindows做的很非常周到的。复杂对象都实现了完备的dispose方法,用来作什么呢? 在被调用时,首先切断DHTML对象实例和脚本对象实例的引用链;清除全局cache变量中的数据,使用delete关键字;使用attachEvent方式导入的事件处理函数,需要detach;其它事件处理回调,使用赋null的方式清空;切断脚本对象之间的parent或child关系引用链。

    这里有点使人迷惑的是,IE的GC的触发是不确定的(目前知道的确定触发就是最小化IE窗口),就是你做好了上述工作,在你的页面刚onload时,内存也是不会立即释放的。不过一段时间使用后,IE使用的内存会减少。所以就不用怀疑先前讨论的方法了,并且除了"切断脚本对象之间的parent或child关系引用链"这一点外,Bindows的dispose的原理和处理方法我前面讨论基本一致。

    注:PM物理内存,VM虚拟内存。都可以在任务管理器中查看。

===========================================================================================

另外一篇:一、

===========================================================================================

JScript的内存泄漏

原文http://javascript.crockford.com/memory/leak.html

当一个系统不能正确的管理他的内存分配时,这个系统就存在内存泄漏,这对系统来说是一个bug。内存泄漏的现象可以有程序调用失败、执行减慢等。

微软的Internet Explorer就存在一系列的内存泄漏,其中最严重的就算是执行Jscript时产生的泄漏。当一个DOM对象包涵有一个JavaScript对象(例如一个事件处理函数)的引用,同时如果这个JavaScript对象又包涵该DOM对象,那么这个循环引用就形成了。这种结构本质上没有问题。此时,因为该DOM对象和这个事件处理函数并没有别的引用存在,那么垃圾回收器(一种自动的内存资源管理器)本应该把它们都回收点,并内存释放。JavaScript的垃圾回收器能够检测到这种循环引用,并不会对他产生困惑。但是不幸的是,IE DOM的内存并不能被Jscript所管理。他有他自己的内存管理系统,然而这套系统并不知道循环引用,使得一切都变得混乱。这就导致了,当循环引用形成的时候,内存释放工作不能完成,也就是产生了内存泄漏。长时间的内存泄漏就将产生内存的匮乏,使得浏览器因缺乏必要内存而崩溃?

我们可以来演示一下。在第一段程序-question1中,我们将动态创建以10个为一组共计10000个的DOM元素(<span>),创建10个然后删除在创建10个,如此循环。你在运行这段程序时打开Windows任务管理器,就可以观察到页面运行时PF(虚拟内存)使用率一直保持不变。PF使用率的变化可以视为内存分配是否无效的指标。

 

Question1

<html>
    <head>
        <title>Queue Test 1</title>
    </head>
    <body>
        <script>
            /*global setTimeout */
            (function (limit, delay) {
                var queue = new Array(10);
                var n = 0;
 
                function makeSpan(n) {
                    var s = document.createElement('span');
                    document.body.appendChild(s);
                    var t = document.createTextNode(' ' + n);
                    s.appendChild(t);
                    return s;
                }
 
                function process(n) {
                    queue.push(makeSpan(n));
                    var s = queue.shift();
                    if (s) {
                        s.parentNode.removeChild(s);
                    }
                }
 
                function loop() {
                    if (n < limit) {
                        process(n);
                        n += 1;
                        setTimeout(loop, delay);
                    }
                }
 
                loop();
            })(10000, 10);
        </script>
    </body>
</html>
 

接下来我们运行第二段程序queuetest2。除了做与queuetest1相同的事情以外,它还未每个元素添加了一个点击事件响应函数。在MozilaOpera上,虚拟PF利用率和queuetest1是一样的,但是在IE上我们可以看见由于内存泄漏而产生的每秒一兆的虚拟内存的稳定增量,通常这种泄露都不会被注意到。但是由于Ajax的日益流行,使得页面在浏览器的停留时间增长,使得问题变得常见了。

Question2

<html>

    <head><title>Queue Test 2</title>

    </head>

    <body>

        <script>

            /*global setTimeout */

            (function (limit, delay) {

                var queue = new Array(10);

                var n = 0;

 

                function makeSpan(n) {

                    var s = document.createElement('span');

                    document.body.appendChild(s);

                    var t = document.createTextNode(' ' + n);

                    s.appendChild(t);

                    s.onclick = function (e) {

                        s.style.backgroundColor = 'red';

                        alert(n);

                    };

                    return s;

                }

 

                function process(n) {

                    queue.push(makeSpan(n));

                    var s = queue.shift();

                    if (s) {

                        s.parentNode.removeChild(s);

                    }

                }

 

                function loop() {

                    if (n < limit) {

                        process(n);

                        n += 1;

                        setTimeout(loop, delay);

                    }

                }

 

                loop();

            })(10000, 10);

        </script>

    </body>

</html>

 

因为IE不能对循环引用进行回收,所以这个任务就落在了我们的肩上。如果我们明确的打破这个循环引用,那么IE就能够完成垃圾回收工作了。具微软的解释,引起内存泄漏的原因是闭包,然而这个结论肯定是非常错误的,并且这使得微软给开发者的建议也成了错误的建议。那么通过DOM来打破循环引用更简单,因为实际上不可能通过Jscript来实现。

当我们处理完一个元素后,我们必须通过把它所有的事件处理函数制空来达到破坏循环引用的目的。我们所需要做的就是把每个事件的处理函数设为空就可以了。我们甚至可以清理函数来完成这一工作。

清理函数将保存一份DOM元素的引用。它将循环检测这个元素的所有属性。如果发现了时间处理函数,就把它值为空。这样就破坏了循环引用,使得内存可以被回收释放。它同样也会检测该元素的子元素,打破他们的循环引用。这个清理函数,只在IE中有效果,对于MozillaOpera都无效。不管是用removeChild()或者是设置innerHTML属性的值,都应该在删除元素之前调用清理函数。

function purge(d) {
    var a = d.attributes, i, l, n;
    if (a) {
        l = a.length;
        for (i = 0; i < l; i += 1) {
            n = a[i].name;
            if (typeof d[n] === 'function') {
                d[n] = null;
            }
        }
    }
    a = d.childNodes;
    if (a) {
        l = a.length;
        for (i = 0; i < l; i += 1) {
            purge(d.childNodes[i]);
        }
    }
}

那么我们现在来运新第3个程序,queuetest3,在程序3里,元素在被删除之前都调用了清理函数。

Question3

<html>

    <head><title>Queue Test 3</title>

    </head>

    <body>

        <p>

            Queue Test 3 adds an event handler to each span, and removes it when

finished. See <a href="http://www.crockford.com/javascript/memory/leak.html">http://www.crockford.com/javascript/memory/leak.html</a>

        </p>

        <script>

            /*global onunload, setTimeout */

            (function (limit, delay) {

                var queue = new Array(10);

                var n = 0;

 

                function makeSpan(n) {

                    var s = document.createElement('span');

                    document.body.appendChild(s);

                    var t = document.createTextNode(' ' + n);

                    s.appendChild(t);

                    s.onclick = function (e) {

                        s.style.backgroundColor = 'red';

                        alert(n);

                    };

                    return s;

                }

 

                function purge(d) {

                    var a = d.attributes, i, l, n;

                    if (a) {

                        l = a.length;

                        for (i = 0; i < l; i += 1) {

                            n = a[i].name;

                            if (typeof d[n] === 'function') {

                                d[n] = null;

                            }

                        }

                    }

                    a = d.childNodes;

                    if (a) {

                        l = a.length;

                        for (i = 0; i < l; i += 1) {

                            purge(d.childNodes[i]);

                        }

                    }

                }

 

                function process(n) {

                    queue.push(makeSpan(n));

                    var s = queue.shift();

                    if (s) {

                        purge(s);

                        s.parentNode.removeChild(s);

                    }

                }

 

                function loop() {

                    if (n < limit) {

                        process(n);

                        n += 1;

                        setTimeout(loop, delay);

                    }

                }

                onunload = function (e) {

                    purge(document.body);

                };

 

                loop();

            })(10000, 10);

        </script>

    </body>

</html>

 

更新:微软发布了该问题的补丁:929874。如果你有十足的信心确保你所有的用户都可以获得该更新,那么你将不再需要上面的清理函数。但不幸的是,这不可能如你所愿,所以可能清理工作在IE6被淘汰之前还是有必要的。

这就是web的天性,有清理不完的bugs

 

===========================================================================================

另外一篇:二、

===========================================================================================

 

在IE下的JS编程中,以下的编程方式都会造成即使关闭IE也无法释放内存的问题,下面分类给出:

1、给DOM对象添加的属性是一个对象的引用。范例:
var MyObject = {};
document.getElementById('myDiv').myProp = MyObject;
解决方法:
在window.onunload事件中写上: document.getElementById('myDiv').myProp = null;


2、DOM对象与JS对象相互引用。范例:
function Encapsulator(element) {
this.elementReference = element;
element.myProp = this;
}
new Encapsulator(document.getElementById('myDiv'));
解决方法:
在onunload事件中写上: document.getElementById('myDiv').myProp = null;


3、给DOM对象用attachEvent绑定事件。范例:
function doClick() {}
element.attachEvent("onclick", doClick);
解决方法:
在onunload事件中写上: element.detachEvent('onclick', doClick);


4、从外到内执行appendChild。这时即使调用removeChild也无法释放。范例:
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
document.body.appendChild(parentDiv);
parentDiv.appendChild(childDiv);
解决方法:
从内到外执行appendChild:
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
parentDiv.appendChild(childDiv);
document.body.appendChild(parentDiv);


5、反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)。范例:
for(i = 0; i < 5000; i++) {
hostElement.text = "asdfasdfasdf";
}
这种方式相当于定义了5000个属性!
解决方法:
其实没什么解决方法:P~~~就是编程的时候尽量避免出现这种情况咯~~


说明:
1、以上资料均来源于微软官方的MSDN站点,链接地址:
http://msdn.microsoft.com/librar ... e_leak_patterns.asp
大家可以到上面这个地址中看到详细的说明,包括范例和图例都有。只是我英文不太好,看不太懂,如果我上述有失误或有需要补充的地方请大家指出。

2、对于第一条,事实上包括 element.onclick = funcRef 这种写法也算在其中,因为这也是一个对对象的引用。在页面onunload时应该释放掉。

3、对于第三条,在MSDN的英文说明中好像是说即使调用detachEvent也无法释放内存,因为在attachEvent的时候就已经造成内存“LEAK”了,不过detachEvent后情况还是会好一点。不知道是不是这样,请英文好的亲能够指出。

4、在实际编程中,这些内存问题的实际影响并不大,尤其是给客户使用时,客户对此绝不会有察觉,然而这些问题对于程序员来说却始终是个心病 --- 有这样的BUG心里总会觉得不舒服吧?能解决则给与解决,这样是最好的。事实上我在webfx.eae.net这样顶级的JS源码站点中,在它们的源码里都会看到采用上述解决方式进行内存的释放管理。

 

===========================================================================================

另外一篇:三、

===========================================================================================

 

在ie的内存泄露中跨页面的泄露是最严重的,浏览器刷新了仍然无法释放掉泄露占用的资源,造成访问速度越来越慢,内存占用越来越大
closure引起cross page leak的主要原因是closure和dom元素的互相引用
看这个例子:

Js代码
  1. <div id="bb"><div id="aa">cc</div></div>   
  2. <script type="text/javascript">   
  3. function leakTest(){   
  4.      var a=[];//用来加大闭包资源占用,方便观察   
  5.     for(var i=0;i<100000;i++){   
  6.       a.push('a');   
  7.      }   
  8.      var divA=document.getElementById('aa');   
  9.      divA.kk=function(){};   
  10.      divA.parentNode.removeChild(divA);   
  11. }   
  12. leakTest();   
  13. </script>  


用sIEve看下发现这个页面每次刷新都会产生跨页面泄露,ie内存占用大了7MB,具体fins的文章中有过介绍
在这个例子中我们在leakTest()中创建了一个内部匿名函数并在dom元素aa的自定义属性kk中保存了他的引用,这就产生了一个闭包
divA.parentNode.removeChild(divA);
这句是产生泄露的主要原因,移除了aa并不会使这个节点消失,只不过在dom树上无法访问这个节点而已,由于闭包的存在divA这个局部变量不会被释放,而divA中保存着aa的引用,这就形成了一个循环引用,闭包保存了dom元素aa的引用,dom元素aa的自定义属性kk又保存了闭包内部的匿名函数的引用,所以在页面刷新的时候IE无法释放掉这个aa和闭包的资源,在我这个例子中就比较吓人,刷一下涨几MB内存
让我们删掉divA.parentNode.removeChild(divA);这句试试,发现没有泄露发生
我推测IE在刷新时会强行释放掉dom树上的元素,而不存在于dom树中的节点不会强行释放,所以造成了跨页面泄露,这是我的个人推测,有别的意见欢迎讨论
怎么解决这个问题呢,其实我们只要打断引用链就行了

Js代码
  1. <div id="bb"><div id="aa">cc</div></div>   
  2. <script type="text/javascript">   
  3. function leakTest(){   
  4.      var a=[];   
  5.     for(var i=0;i<100000;i++){   
  6.       a.push('a');   
  7.      }   
  8.      var divA=document.getElementById('aa');   
  9.      divA.kk=function(){};   
  10.      divA.parentNode.removeChild(divA);   
  11.      divA=null;   
  12. }   
  13. leakTest();   
  14. </script>  


或者

Js代码
  1. <div id="bb"><div id="aa">cc</div></div>   
  2. <script type="text/javascript">   
  3. function leakTest(){   
  4.      var a=[];   
  5.     for(var i=0;i<100000;i++){   
  6.       a.push('a');   
  7.      }   
  8.      document.getElementById('aa').kk=function(){};   
  9.      document.getElementById('aa').parentNode.removeChild(document.getElementById('aa'));   
  10.     //这个例子不保存aa的应用,也不会引起泄露   
  11. }   
  12. leakTest();   
  13. </script>  


or

Js代码
  1. <div id="bb"><div id="aa">cc</div></div>   
  2. <script type="text/javascript">   
  3. function leakTest(){   
  4.      var a=[];   
  5.     for(var i=0;i<100000;i++){   
  6.       a.push('a');   
  7.      }   
  8.      var divA=document.getElementById('aa');   
  9.      divA.kk=function(){};   
  10.      divA.parentNode.removeChild(divA);   
  11.     return divA;   
  12. }   
  13. var divA=leakTest();   
  14. divA.kk=null; //这个可以看到内存占用比上面少了7MB,因为解除了对闭包内部函数的引用,闭包占用的资源被释放了   
  15. </script>  


通过上面的例子可以看出,如果某个函数中dom元素保存了内部函数的引用,就会形成闭包,很容易引起泄露,务必小心
另firefox下测试是没有这些问题的

 

===========================================================================================

另外一篇:四、

===========================================================================================

  最近做一个公司的业务系统,公司要求能尽可能的与c/s近似,也就是如c/s一样,点击文本框可以弹出此项目的相关内容,进行选择输入。

  我使用了弹出窗口,然后在子窗口双击选中项目,把选中的值返回给父窗体。

  在系统做完了之后,在客户使用的过程,由于客户使用的是512m的内存配置,所以在打开了30--40个窗体之后,ie的虚拟内存占用量达到近200m,从而使系统变慢,javascript的运行也变慢了。

  我使用任务管理器,打开一个弹出窗口,ie内存就增加1-3m,然后关闭窗口,有时内存并不释放,有时才释放几十k。看来问题出在了内存释放上面。

  接着按内存释放这个思路,进行搜索查找方法,来进行解决这个问题。我找到一个javascript未公开的函数CollectGarbage,这个函数是用来进行内存释放的。我在所有的弹出窗口结束之前把所有的自己定义的javasctip的变量设置为null,并调用CollectGarbage函数。

  javascript中把变量设为null,javascript并不会把内存释放,当下次再次定义变量时,就会覆盖此变量所在的内存。如果不设为null,javascript再次定义变量时,会开辟一个新的内存空间。

  在使用以上处理之后,再次打开窗口,ie的内存每次还是增加1-3m,但是在关闭窗口之后,则ie会释放一定数量的内存在500k至2m。起到了一定的作用。
       由于我在页面中使用了第三方的控件,第三方的控件中的javascript中的内存是如何管理,就不是由我来控制的了。
     
  1.javascript内存释放的方法示例

  把所有上级函数的参数即使设为null,并使用CollectGarbage来释放内存。  

 示例

  <script>    
  //32M  
  function   AllocMem()  
  {  
  var   str="12345678";  
  for(var   i=3;i<24;i++)  
  str+=str;  
  return   str;  
  }      
  function   A(a)  
  {  
  a=null;  
  return   r;  
  function   r()  
  {  
  }  
  }      
  var   f=A(AllocMem());  
  alert(1);  
  CollectGarbage();  
  //明显,已经释放了。  
  r=null;  
  alert(2);  
  CollectGarbage();     
  </script>  

  里面对于内存释放的规则(脚本层)已经理解得很透了。  
  (每一层菜单分配?M的内存.对着任务管理器才看到情况)  

注:
CollectGarbage()通常会在核心推出内存,因就是IE或NS程序结束的时候才会调用.这样才是安全的  

说明:
1)   如果你在另一个window中keep了该window中的object的reference,即使关闭该window,内存也没有释放  
  As   you   might   know,   windows   opened   with   window.open()   may   share   a   process   with   its   opener   (_blank   or   _new     window   may   not).   That   is,   even   if   you   see   those   two   windows   on   the   desktop,   if   you   look   at   the   process   table   in   the   Task   Manager,   you   may   only   see   one   IEXPLORE.EXE   running.   Memory   may   only   be   released   when   the   process   is   terminated  
   
2)更糟糕的是,如果你keep的是一个DOM   object的reference,   关闭该object   所在window,   IE会crash,   报内存错误(或者要求,重新启动)  
   
  I   would   say   this   looks   like   a   bug,   you   might   want   to   report   to   Microsoft 

 

===========================================================================================

 

 

 

 

 

 

 

 

 

 

 

 

 

======================================================================================

分析:

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

第一:避免“循环引用(Circular References)

      避免“js对象实例跟dom对象的相互引用(不要将一个DOM对象和一个JS对象相互成为对方的属性)”,即切断DHTML对象实例和脚本对象实例的引用链

      原理:microsoftie浏览器有许多内存泄漏问题,最严重的就是使用jscript交互引发。当dom节点包含了一个对javascript对象(如事件处理函数onclick)的引用,同时该javascript对象也引用此节点时(如鼠标单击时隐藏节点),就形成了一个循环结构。它本身没有问题——当对dom节点和事件处理函数的引用消失,garbage collector(一个自动的内存资源管理器)会回收它们,使它们占用的空间再分配。javascriptgarbage collector理解循环,不会产生问题。不幸的是,iedom不归jscript管。ie有自己的内存管理器却不理解循环,因此产生混乱。结果,当循环出现,却没有内存回收,没被回收的内存就泄漏了。时间长了,就会导致内存不足,当内存空间涨满,浏览器就死了。”

 

       比如以前为了方便,在涉及操作dom对象的地方,通常建立一个对dom对象的引用,dom对象也建立一个指向js对象实例的引用,例如:

       function myObject() 

       {

           this.element = document.getElementById('myElement');

           document.getElementById('myElement').instance = this;

       }

       var obj = new myObject();

       这是应该避免的。如不能避免,则在页面卸载前document.getElementById('myElement').instance = null来破坏引用链从而使用内存正常清理。

    又比如  s.parentNode.removeChild(s);  也是要避免的

    解决方法一、在删除含有对javascript对象引用的dom节点之前,把属性中类型为function的全部清除(设为null)。我试过,确实有效。

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

第二:避免“内部函数引用(Closures)

     解决方法一、将事件处理函数放在定义它的函数的外部

     解决方法二、使用attachEvent方式导入的事件处理函数,需要detach

                 其它事件处理回调,使用赋null的方式清空;

                     比如:使用obj.onclick = function(){.....;}

                           如需要释放则写成 obj.onclick = function(){.....;  obj.onclick = null ;}

                           又比如:

function DoThis()

{

  var el = document.createElement("div");

  el.attachEvent("onclick",DoThis);

  window.attachEvent("onunload",Cleanup);

  function DoThis()

  {

    alert("clicked");

  }

  function Cleanup()

  {

    el.detachEvent("onclick",DoThis);

    window.detachEvent("onunload",Cleanup);

    el = null;

  }

}

 

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

第三:避免“页面交叉泄漏(Cross-Page Leaks)”,关键在于HTML元素的向DOM插入对象顺序

      插入顺序有两种:

      第一:(称为逆序插入)子节点插入JS的父节点对象,父节点对象再插入DOM

      第二:(称为顺序插入)父节点插入DOM,子节点再插入DOM中的父节点。

      王某人测试:循环5000次插入两个父子节点

          第一次:逆序插入 + 不带script

                            var parentDiv = document.createElement("<div'>");

                var childDiv = document.createElement("<div>");

                parentDiv.appendChild(childDiv);

                hostElement.appendChild(parentDiv);

          第二次:顺序插入 + 不带script

                            var parentDiv = document.createElement("<div'>");

                var childDiv = document.createElement("<div>");

                hostElement.appendChild(parentDiv);

                parentDiv.appendChild(childDiv);

          第三次:逆序插入 + script

                            var parentDiv = document.createElement("<div οnclick="***()"'>");

                var childDiv = document.createElement("<div οnclick="***()">");

                parentDiv.appendChild(childDiv);

                hostElement.appendChild(parentDiv);

          第四次:顺序插入 + script

                            var parentDiv = document.createElement("<div οnclick="***()"'>");

                var childDiv = document.createElement("<div οnclick="***()">");

                hostElement.appendChild(parentDiv);

                parentDiv.appendChild(childDiv);

               结果:点有内存数                  执行时     刷新后         是否加上removeChildNULL

                第一次   刚打开3907584    61687032 -   4947968  

                   n   刚打开3907584    61465448 -   4653056

                  r n  刚打开3907584   162885632 -  67342336 再刷新 5857280 

                            第二次: 刚打开3944448    61169296 -   4894720

                  n   刚打开3907584    61280546 -   4448256

                  r n  刚打开3907584   159997952 -  64135168 再刷新 5689344 

                            第三次: 刚打开3944448   122789888 -   6479872  

                  n   刚打开3907584   122822656 -   5300224           

                  r n  刚打开3907584   221663232 -  77639680 再刷新 5976201

                            第四次: 刚打开3944448    69128192 -   5844992    

                  n   刚打开3907584    69072828 -   5230592     

                  r n  刚打开3907584   169172184 -  76465856 再刷新 6254592

 

          测试数据证明:1、加NULL是有作用的,本例加上parentDiv=null;childDiv=null;

                                          2、添入DOM的元素如果不带script,顺序插入占用内存比逆序只是稍微小一点,

                           但是如果带script,顺序比逆序占用内存要小得多,所以使用顺序插入总是最好的。

               1:网上有人提出解决方案是:“不要在创建DOM对象时插入script”,看来是没必要的,只要我们遵循顺序插入,就不用考虑有没有带script了,而且此功能还是经常用到,避免不了不去使用。

                     2:网上有人又提出“切断脚本对象之间的parentchild关系引用链”,顺序插入原理即如此。

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

第四:上面Douglas Crockford的文章写了一个函数

function Purge(d)  

{

                     var a = d.attributes, i, l, n; 

                     if (a)

                     {      

                            l = a.length;       

                            for (i = 0; i < l; i += 1)

                            {            

                                   n = a[i].name;           

                                   if (typeof d[n] === 'function')

                                   {               

                                          d[n] = null;           

                                   }       

                            }   

                     }   

                     a = d.childNodes;   

                     if (a)

                     {       

                            l = a.length;       

                            for (i = 0; i < l; i += 1)

                            {           

                                   Purge(d.childNodes[i]);       

                            }   

                     }

}

       使用:如purge(document.body); 对参数中的对象进行遍历,给相关的JS对象属设空,但这样影响效率,如Douglas Crockford文章里面,使用了purge()比没有使用的占用内存多了两倍,所以用的时候也要取舍。

在这篇论坛http://topic.csdn.net/u/20071109/09/eae5ded3-54ab-4138-b1d0-d3507ceca708.html,也有这个思路,但有人去回复道:

                         其实IE是有内置的内存释放模式的 只要你将相应的引用设置为null IE就会在适当的时候去对其重新分配  

                         HTML元素内的function成员是个例外 如果你不手动设置成null 就算卸载了这个HTML元素 IE也不会去主动释放它 包括function内引用到的所有内存都不会被释放

                            btbtd的方法确实是个万全之策,不过效率比较低.毕竟要遍历所有HTML节点的所有属性.

                            建议LZ如果是你自己写的程序 最好手动的去卸载这些function 好像你上面的程序 只要加个οnclick=null就行了

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

第五:使用CollectGarbage();

      但应该注意:cg的条件必备:1、离开上下文环境   2、没有引用

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

第六:清除全局cache变量中的数据,使用delete关键字

      这一点还不是很明白。

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

第七:尽量不要使用IE透明滤镜,我发现当插入一个长宽各为1000px并应用透明滤镜的div时,内存占用增加大约4MB

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

总结:

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

第一:避免“循环引用(Circular References)

      避免“js对象实例跟dom对象的相互引用(不要将一个DOM对象和一个JS对象相互成为对方的属性)”,即切断DHTML对象实例和脚本对象实例的引用链

      1、不出现循环引用。

                     比如:s.parentNode.removeChild(s);  要避免

2、出现循环引用,用NULL解决

      比如:在删除含有对js对象引用的dom节点之前,把属性中类型为function的全部清除,下面的purge()原理即如此!比如purge(document.body)

                 比如:function myObject() 

                              {

                                       this.element = document.getElementById('myElement');

                                       document.getElementById('myElement').instance = this;

                           }

                              var obj = new myObject();

                          这是应该避免的。如不能避免,则在页面卸载前用

document.getElementById('myElement').instance = null来破坏引用链从而使用内存正常清理

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

第二:避免“内部函数引用(Closures)

     1、尽量将事件处理函数放在定义它的函数的外部

     2、使用attachEvent方式导入的事件处理函数,需要detach

     3、其它事件处理回调,使用赋null的方式清空;

            比如:使用obj.onclick = function(){.....;}

                  如需要释放则写成 obj.onclick = function(){.....;  obj.onclick = null ;}

                  又比如:

function DoThis()

{

var el = document.createElement("div");

el.attachEvent("onclick",DoThis);

window.attachEvent("onunload",Cleanup);

function DoThis()

  {    alert("clicked");    }

function Cleanup()

  {    el.detachEvent("onclick",DoThis);

       window.detachEvent("onunload",Cleanup);

       el = null;    }

}

 

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

第三:避免“页面交叉泄漏(Cross-Page Leaks)

1、  父子节点插入DOM,使用顺序插入

2、  创建插入时尽量不带有script

比如不要用 var parentDiv = document.createElement("<div οnclick="***()"'>");

                         hostElement.appendChild(parentDiv);

                       但可以先创建,后写script

            如:var parentDiv = document.createElement("<div>");

                         hostElement.appendChild(parentDiv);

                                           parentDiv.οnclick=***;

                                           但应该注意,本例只解决Cross-Page Leaks,还要解决“循环引用”“闭包”问题

 

 

 

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

第四:使用上面purge(document.body);在页面卸载时清理,这样会影响效率,不得已时而用之

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

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值