设为首页收藏本站订阅更新

无忧脚本

 找回密码
 加入无忧

QQ登录

只需一步,快速开始

搜索
查看: 19163|回复: 17

在IE下的JS编程需注意的内存释放问题

[复制链接]
发表于 2005-12-30 15:08:32 | 显示全部楼层 |阅读模式
在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源码站点中,在它们的源码里都会看到采用上述解决方式进行内存的释放管理。

[ 本帖由 mingyuan5 最后编辑于 2005-12-30 15:19 ]
发表于 2005-12-30 20:15:46 | 显示全部楼层
已加入索引,谢谢对本版的支持~!
发表于 2006-12-14 17:07:03 | 显示全部楼层
釋放内存,就是把引用對象給釋放掉(在堆中分配的内存)。
发表于 2007-1-18 13:18:28 | 显示全部楼层
非常之感谢!!!
我正在为这个事情而头疼啊!!!


跪谢辣!!!!
发表于 2007-1-19 09:11:59 | 显示全部楼层

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

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对象实例的状态是脚本无法得知的。下面是它们的基本原则:

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

    本模型中引起的泄漏问题基于COM的引用计数。脚本引擎对象会维持对DOM对象的引用,并在清理和释放DOM对象指针前等待所有引用的移除

。在我们的示例中,我们的脚本引擎对象上有两个引用:脚本引擎作用域和DOM对象的expando属性。当终止脚本引擎时第一个引用会释放,DOM

对象引用由于在等待脚本擎的释放而并不会被释放。你可能会认为检测并修复假设的这类问题会非常的容易,但事实上这样基本的的示例只是

冰山一角。你可能会在30个对象链的末尾发生循环引用,这样的问题排查起来将会是一场噩梦。

    如果你仍不清楚这种泄漏方式在HTML代码里到底怎样,你可以通过一个全局脚本变量和一个DOM对象来引发并展现它。
  1. <html>
  2.     <head>
  3.         <script language="JScript">
  4.         var myGlobalObject;
  5.         function SetupLeak()
  6.         {
  7.             // First set up the script scope to element reference
  8.             myGlobalObject = document.getElementById("LeakedDiv");

  9.             // Next set up the element to script scope reference
  10.             document.getElementById("LeakedDiv").expandoProperty = myGlobalObject;
  11.         }

  12.         function BreakLeak()
  13.         {
  14.             document.getElementById("LeakedDiv").expandoProperty = null;
  15.         }
  16.         </script>
  17.     </head>
  18.     <body onload="SetupLeak()" onunload="BreakLeak()">
  19.         <div id="LeakedDiv"></div>
  20.     </body>
  21. </html>
复制代码
   你可以使用直接赋null值得方式来破坏该泄漏情形。在页面文档卸载前赋null值,将会让脚本引擎知道对象间的引用链没

有了。现在它将能正常的清理引用并释放DOM对象。在这个示例中,作为Web开发员的你因该更多的了解了对象间的关系。

    作为一个基本的情形,循环引用可能还有更多不同的复杂表现。对基于对象的JScript,一个通常用法是通过封装JScript对象来扩充DOM对

象。在构建过程中,你常常会把DOM对象的引用放入JScript对象中,同时在DOM对象中也存放上对新近创建的JScript对象的引用。你的这种应

用模式将非常便于两个对象之间的相互访问。这是一个非常直接的循环引用问题,但是由于使用不用的语法形式可能并不会让你在意。要破环

这种使用情景可能变得更加复杂,当然你同样可以使用简单的示例以便于清楚的讨论。
  1. <html>
  2.     <head>
  3.         <script language="JScript">

  4.         function Encapsulator(element)
  5.         {
  6.             // Set up our element
  7.             this.elementReference = element;

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

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

  16.         function BreakLeak()
  17.         {
  18.             document.getElementById("LeakedDiv").expandoProperty = null;
  19.         }
  20.         </script>
  21.     </head>
  22.     <body onload="SetupLeak()" onunload="BreakLeak()">
  23.         <div id="LeakedDiv"></div>
  24.     </body>
  25. </html>
复制代码
   更复杂的办法还有记录所有需要解除引用的对象和属性,然后在Web文档卸载的时候统一清理,但大多数时候你可能会再造

成额外的泄漏情形,而并没有解决你的问题。

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


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

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

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

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

  8.             function ClickEventHandler()
  9.             {
  10.                 // This closure refs element
  11.             }
  12.         }

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

  18.         function BreakLeak()
  19.         {
  20.         }
  21.         </script>
  22.     </head>
  23.     <body onload="SetupLeak()" onunload="BreakLeak()">
  24.         <div id="LeakedDiv"></div>
  25.     </body>
  26. </html>
复制代码


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

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

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

  11.             function ClickEventHandler()
  12.             {
  13.                 // This closure refs element
  14.             }
  15.         }

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

  21.         function BreakLeak()
  22.         {
  23.             document.getElementById("LeakedDiv").detachEvent("onclick",
  24.                 document.getElementById("LeakedDiv").expandoClick);
  25.             document.getElementById("LeakedDiv").expandoClick = null;
  26.         }
  27.         </script>
  28.     </head>
  29.     <body onload="SetupLeak()" onunload="BreakLeak()">
  30.         <div id="LeakedDiv"></div>
  31.     </body>
  32. </html>
复制代码


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

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

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

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

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

  4.         function LeakMemory()
  5.         {
  6.             var hostElement = document.getElementById("hostElement");

  7.             // Do it a lot, look at Task Manager for memory response

  8.             for(i = 0; i < 5000; i++)
  9.             {
  10.                 var parentDiv =
  11.                     document.createElement("<div onClick='foo()'>");
  12.                 var childDiv =
  13.                     document.createElement("<div onClick='foo()'>");

  14.                 // This will leak a temporary object
  15.                 parentDiv.appendChild(childDiv);
  16.                 hostElement.appendChild(parentDiv);
  17.                 hostElement.removeChild(parentDiv);
  18.                 parentDiv.removeChild(childDiv);
  19.                 parentDiv = null;
  20.                 childDiv = null;
  21.             }
  22.             hostElement = null;
  23.         }


  24.         function CleanMemory()
  25.         {
  26.             var hostElement = document.getElementById("hostElement");

  27.             // Do it a lot, look at Task Manager for memory response

  28.             for(i = 0; i < 5000; i++)
  29.             {
  30.                 var parentDiv =
  31.                     document.createElement("<div onClick='foo()'>");
  32.                 var childDiv =
  33.                     document.createElement("<div onClick='foo()'>");

  34.                 // Changing the order is important, this won't leak
  35.                 hostElement.appendChild(parentDiv);
  36.                 parentDiv.appendChild(childDiv);
  37.                 hostElement.removeChild(parentDiv);
  38.                 parentDiv.removeChild(childDiv);
  39.                 parentDiv = null;
  40.                 childDiv = null;
  41.             }
  42.             hostElement = null;
  43.         }
  44.         </script>
  45.     </head>

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

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

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

    象DOM插入顺序问题那样,这个问题也需要依赖创建临时对象来产生"泄漏"。对一个脚本元素对象内部的脚本文本一而再再而三的反复重写,慢慢地你将开始泄漏各种已关联到被覆盖内容中的脚本引擎对象。特别地,和脚本调试有关的对象被作为完全的代码对象形式保留了下来。
  1. <html>
  2.     <head>
  3.         <script language="JScript">
  4.         function LeakMemory()
  5.         {
  6.             // Do it a lot, look at Task Manager for memory response
  7.             for(i = 0; i < 5000; i++)
  8.             {
  9.                 hostElement.text = "function foo() { }";
  10.             }
  11.         }
  12.         </script>
  13.     </head>
  14.     <body>
  15.         <button onclick="LeakMemory()">Memory Leaking Insert</button>
  16.         <script id="hostElement">function foo() { }</script>
  17.     </body>
  18. </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.

(翻译来自:http://birdshome.cnblogs.com/archive/2006/05/28/IE_MemoryLeak.html)

[ 本帖最后由 Rimifon 于 2007-1-19 09:31 编辑 ]
发表于 2007-3-26 15:02:11 | 显示全部楼层
:handshake :handshake 长见识了~
发表于 2007-4-2 12:59:00 | 显示全部楼层
最近被这个问题搞得吐血......
发表于 2007-4-10 10:07:28 | 显示全部楼层

闭包内存泄露分析

看了大家的讨论,我也很感兴趣,所以特意去了MSDN了解了下IE解析器小组对闭包的解释,自己做了下研究。觉得挺有意思的,发出来给大家分享下。

该分析不仅仅适用js,凡是可以实现闭包的语言都存在相同问题。

何谓闭包:方法内的局部变量,可以在该方法执行完后(即该方法的作用域外部)被访问。
或者说:方法内局部变量的生命周期超过了方法本身的生命周期。

看js示例1:
function AdderFactory(y) {
  return function(x){return x + y;}
}
var MyFunc;
if (whatever)
  MyFunc = AdderFactory(5);
else
  MyFunc = AdderFactory(10);
print(MyFunc(123)); // Either 133 or 128.
第一点要牢记,js中一切都是对象,方法也是,属性也是,变量也是。

在上例中,方法(对象)AdderFactory内部创建了一个匿名方法(对象)function(x){return x + y;}。并且,我们利用return指令将该匿名方法的一个引用扔给AdderFactory方法外部使用,比如可以用于赋值。
如果变量whatever为true,则语句MyFunc = AdderFactory(5);执行完后,变量MyFunc就被赋予了AdderFactory对象中用return扔出的匿名对象的引用。
一般情况下,AdderFactory方法(对象)内部所有变量的生命周期都应该小于该方法的执行时间。但在示例中,由于MyFunc变量保持了对匿名对象的引用,所以该匿名对象在AdderFactory方法执行后不会被GC回收,又由于匿名对象是AdderFactory对象的一个属性,所以同时GC也不会回收AdderFactory对象。
以上示例代码执行完成后的对象引用图如下:



图中矩形和椭圆代表对象,箭头代表引用。

闭包的功能非常强大,上面的示例中也不会造成不好的影响。

但是,滥用闭包将十分可怕。

看js示例2:
<html>
<script language="JavaScript">
<!--
Function closureTest (){
        var maskDiv = document.createElement("div");
        maskDiv.id = "myDiv";
        maskDiv.style.width = "100%";
        maskDiv.style.height = "100%";
        maskDiv.style.position = "absolute";
        maskDiv.style.filter = "alpha(opacity=50)";
        maskDiv.style.backgroundColor = "red";
        maskDiv.oncontextmenu = function(){ return false; };
        document.body.appendChild(maskDiv);
}
//-->
</script>
<body onload=”closureTest();”>
</body>
</html>

为了突出主题,我省略了不影响示例的多余标签。
用IE执行一个如上的页面,打开windows任务管理器,查看iexplore.exe进程的“内存使用”。
按几下F5看看内存是如何增加的。^_^
这就是闭包导致内存泄露问题。

看分析:
这是一个闭包应用。为什么?
看这里,看这里,看这里
maskDiv.oncontextmenu = function(){ return false; };

为了说明问题,看下图:



显然,这里出现了循环引用(实箭头是直接引用,虚箭头是间接引用)。为什么?
我们看看程序执行过程:
①.        页面载入完成后IE调用body的onload事件的绑定方法closureTest(),并以该方法为构造器创建closureTest对象。

然后执行var maskDiv = document.createElement("div");语句。该语句有下面3步,
②.        通过document.createElement("div")方法生成一个div对象,对象id在之后赋予。
③.        通过var maskDiv语句生成一个closureTest的内部变量(对象)maskDiv。
④.        通过“等号”= 赋值,将一个对新建div的引用存入maskDiv变量。

一直到maskDiv.style.backgroundColor = "red";语句都是对div对象的设置,与本例无关。
然后执行maskDiv.oncontextmenu = function(){ return false; };语句。该语句也有下面3步,
⑤.        在closureTest对象内创建一个匿名对象Anonymity,其构造器是function(){ return false; }方法。
⑥.        在IE中,也许oncontextmenu属性是div原有的属性(对象),也许不是,但不管如何,maskDiv.oncontextmenu语句保证myDiv对象必定有该属性(有则用之,无则创建)。
⑦.        通过“等号”= 赋值,将匿名对象的引用存入oncontextmenu对象中。

显然,例2是比例1略微复杂的一个闭包应用。
本来,光是实箭头还不至于产生循环引用,但是由于闭包的使用,导致Anonymity与maskDiv对象出现了间接引用。因为在closureTest()方法执行完后,由于图中⑦的引用的存在,Anonymity对象将继续快乐的生活着,所谓“一佛得道,鸡犬升天”。closureTest对象也要沾点喜气啦,而maskDiv变量的长寿也就顺理成章罗。如此循环往复,无穷尽也。

那么,如果顺其自然,IE何时才会结束他们的圈地运动呢?
答案是关闭当前页面所处的IE窗口时。
但是,可恶的是,如果你是在某些IDE内使用预览功能看页面的话,由于IDE替代IE接管了内存管理,所以,圈地运动的结束被迫延迟到了,关闭IDE时,比如EditPlus。
这太恐怖了,我要手工干掉这些乱炒房地产的开发商。OK,没问题,我们有几十种方法,但为了方便,举例4种典型方法如下:

方法一:移花接木
这是常用的解决办法,将
var maskDiv = document.createElement("div");
语句前的var定义去掉,maskDiv对象将不属于closureTest对象,变成了一个全局变量。
变化后的引用关系图如下:



很显然,圈地运动不可能发展起来了。但其还是有点不尽如人意。
1.        maskDiv是全局变量,当页面复杂时(比如引入了多个js文件),随意污染window对象的全局作用域,将陷入名称空间碰撞的泥潭。
2.        自然情况下,这些对象及引用一旦创建,则只能在页面卸载时被GC回收,有点浪费内存空间。——我们可是有名的铁公鸡。

方法二:战争践踏
在脚本中增加如下代码:
function myGC(){
        document.body.removeChild(document.getElementById("myDiv");
}
在body标签上增加onunload事件处理 onunload=” myGC();”
这有些暴力了,在卸载页面时,我们强行将myDiv给干掉,循环引用自然被打破,皮之不存,毛将焉附!
但是,它的缺点也是明显的,
1.        增加了庞大的代码量
看人家方法一,不但没有增加代码,还去掉了个var,咱可到好,一个div就多写这么多代码,那复杂点的页面还不把人给烦死。
2.        增加了代码耦合
myDiv这个名字这么好听,我closureTest就想自己留着用,凭啥要告诉你个myGC。知道个名字也就算了,还要知道我把她嫁到了body家,郁闷那。

方法三:温柔一刀
比方法二温柔一点,既然不让干那么野蛮的事情,我们把方法二中的
document.body.removeChild(document.getElementById("myDiv");
修改为
document.getElementById("myDiv").oncontextmenu = null;
在页面卸载时,将引用图中的⑦给一刀斩断。不要把问题扩大化,应该在局部解决。
但是,本质上,方法三并不比方法二好很多,庞大的代码量和强耦合还是我们的心头刺。

方法四:Oh,my god! 上帝说,“这很简单”!
我们只需要在
document.body.appendChild(maskDiv);
语句后增加一句话,一切烦恼都烟消云散。
maskDiv = null;
真是太完美了,优点如下:
1.        只是简单的增加了几个字符,
2.        他不会在window对象下增加危险的全局变量,不会出现无聊的命名冲突,
3.        在closureTest()方法执行后,引用图中的③、④就乖乖的让出内存地盘,真应了偶铁公鸡的名号。(一些小气的GC还是不闻不问)
4.        没有增加讨厌的myGC()方法,不需要在body上再绑定onunload事件。赞美神!少敲好多代码啊。
5.        myDiv这名字就我closureTest自己用了,减少了可能的耦合点,赞!


神说:“简单就是美!”

综上所述,闭包之所以容易引起内存泄露,本质是由于其容易引起循环引用。所以,如何发现并避免出现循环引用是我们要关注的重点。不要被所谓闭包的新名词遮蔽我们的双眼。

[ 本帖最后由 hamal 于 2007-4-10 10:14 编辑 ]

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?加入无忧

x
发表于 2007-4-16 12:17:54 | 显示全部楼层

好文!!!

楼上的强,深入浅出,通俗易懂,佩服!

[ 本帖最后由 tearyrose 于 2007-4-16 12:19 编辑 ]
发表于 2007-4-16 16:40:32 | 显示全部楼层
我对8楼的例子测试了下,傲游和firefox都没有问题,只是在ie6下会有问题
这应该是ie的bug吧
发表于 2007-4-20 15:02:41 | 显示全部楼层
今天遇到一个progid:DXImageTransform.Microsoft.Alpha滤镜产生的内存泄露,尚且没有啥好办法
发表于 2008-7-7 15:56:20 | 显示全部楼层
说得不错~
发表于 2009-2-19 15:12:12 | 显示全部楼层
这帖子太好了
发表于 2009-6-1 00:03:13 | 显示全部楼层
真的能用吗?不错啊


-------------------------------------------------------
签名:http://www.0000114.com
发表于 2010-1-2 13:44:42 | 显示全部楼层
好贴,学习了
发表于 2010-1-5 15:00:34 | 显示全部楼层
设置为null 之后,这个变量(对象)本身还存在吗?
发表于 2011-1-18 13:35:49 | 显示全部楼层
对不起,挖千年老坟了...
主楼的第4条和五楼的“Figure 3. DOM插入顺序泄漏模型”顺序不是完全反了吗??
还是我理解有问题??

顺便回楼上,设置为null后对象还是存在的,只是它的引用已经指向特殊值null了,这样它原本引用的内存就可以被释放,请参考下面关于CG的解释


JavaScript则不要求手动地释放内存,它使用一种称为垃圾收集(garbage collection)的方法。JavaScript的解释器可以检测到何时程序不再使用一个对象了。当它确定了一个对象是无用的时候(例如,程序中使用的变量再也无法引用这个对象了),它就知道不再需要这个对象,可以把它所占用的内存释放掉了。例如,考虑下面的几行代码:
var s = "hello"                   // Allocate memory for a string
var u = s.toUpperCase();    // Create a new string
s = u;                              // Overwrite reference to original string
运行了这些代码之后,就不能再获得原始的字符串“hello”,因为程序中没有变量再引用它了。系统检测到这一事实后,就会释放该字符串的存储空间以便这些空间可以被再利用。
---------摘录至《JavaScript 权威指南 第五版》(机械工业出版社出版) P.63


如果要让对象不存在,可以使用delete删除它,但...无法删除使用var声明的变量


并非所有的属性和变量都是可以删除的,某些内部的核心属性和客户端属性不能删除,用var语句声明的用户定义变量也不能被删除。如果delete使用的运算数是一个不存在的属性,它将返回true(令人吃惊的是,ECMAScripte标准规定,当delete运算的运算数不是属性、数组元素或变量时,它返回true)。下面是一些使用该运算符的例子:
var o = {x:1, y:2};           // Define a variable; initialize it to an object
delete o.x;                     // Delete one of the object properties; return true
typeof o.x;                     // Property does not exist; return "undefined"
delete o.x;                     // Delete a nonexistent property; return true
delete o;                       // Cna't delete a declared variable; return false
delete 1;                       // Can't delete an integer; return true
x = 1;                           // Implicitly declare a variable without var keyword
delete x;                       // Can delete this kind of variable; return true
x;                                 // Runtime error: x is not defined

注意,删除属性、变量或数组元素不只是把它们的值设置为undefined。当删除一个属性后,该属性将不再存在。
delete所能影响的只是属性值,并不能影响被这些属性引用的对象,理解这一点很重要。考虑如下的代码
var my = new Object();      // Create an object named "my"
my.hire = new Date();        // my.hire refers to a Date object
my.fire = my.hire;               // my.fire refers to the same object
delete my.hire;                  // hire property is deleted; return true
document.write(my.fire);    // But my.fire still refers to the Date object
---------摘录至《JavaScript 权威指南 第五版》(机械工业出版社出版) P.87

注!如果不使用var声明变量,则自动声明为全局变量
浏览器中,如果不使用var声明变量,实际上相当于给window对象添加一个属性
x = 123;                   // 隐式声明x变量,实际上是给window对象添加一个x属性
alert(window.x)         // output 123
delete x                   // 相当于 delete window.x; return true
alert(window.x)        // 返回"undiefined"
alert(x)                    // 运行时错误,返回x未定义

[[I] 本帖最后由 consatan 于 2011-1-18 14:21 编辑 [/I]]
您需要登录后才可以回帖 登录 | 加入无忧

本版积分规则

小黑屋|手机版|Archiver|无忧脚本 ( 苏ICP备05080427号 )|值班电话:027-62300445   鄂公网安备 42011102000433号

GMT+8, 2017-11-23 19:11 , Processed in 0.109055 second(s), 8 queries , Gzip On, Memcache On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表