Js内存泄漏及解决方案

在IE下的JS编程中,如下的编程方式都会形成即便关闭IE也没法释放内存的问题,下面分类给出: 

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


二、DOM对象与JS对象相互引用。范例: 
function Encapsulator(element) { 
this.elementReference = element; 
element.myProp = this; 

new Encapsulator(document.getElementById('myDiv')); 
解决方法: 
在onunload事件中写上: document.getElementById('myDiv').myProp = null; 


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


四、从外到内执行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); 


五、反复重写同一个属性会形成内存大量占用(但关闭IE后内存会被释放)。范例: 
for(i = 0; i < 5000; i++) { 
hostElement.text = "asdfasdfasdf"; 

这种方式至关于定义了5000个属性! 
解决方法: 
其实没什么解决方法:P~~~就是编程的时候尽可能避免出现这种状况咯~~ 


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

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

三、对于第三条,在MSDN的英文说明中好像是说即便调用detachEvent也没法释放内存,由于在attachEvent的时候就已经形成内存“LEAK”了,不过detachEvent后状况仍是会好一点。不知道是否是这样,请英文好的亲可以指出。 

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

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


Web开发的发展 

在过去一些的时候,Web开发人员并无太多的去关注内存泄露问题。那时的页面间联系大都比较简单,并主要使用不一样的链接地址在同一 

个站点中导航,这样的设计方式是很是有利于浏览器释放资源的。即便Web页面运行中真的出现了资源泄漏,那它的影响也是很是有限并且经常 

是不会被人在乎的。 

今天人们对Web应用有了高更的要求。一个页面极可能数小时不会发生URL跳转,并同时经过Web服务动态的更新页面内容。复杂的事件关联 

设计、基于对象的JScript和DHTML技术的普遍采用,使得代码的能力达到了其承受的极限。在这样的状况和改变下,弄清楚内存泄露方式变得 

很是的急迫,特别是过去这些问题都被传统的页面导航方法给屏蔽了。 

还算好的事情是,当你明确了但愿寻找什么时,内存泄露方式是比较容易被肯定的。大多数你能遇到的泄露问题咱们都已经知道,你只需 

要少许额外的工做就会给你带来好处。虽然在一些页面中少许的小泄漏问题仍会发生,可是主要的问题仍是很容易解决的。 

泄露方式 

在接下来的内容中,咱们会讨论内存泄露方式,并为每种方式给出示例。其中一个重要的示例是JScript中的Closure技术,另外一个示例是 

在事件执行中使用Closures。当你熟悉本示例后,你就能找出并修改你已有的大多数内存泄漏问题,可是其它Closure相关的问题可能又会被忽 

视。 

如今让咱们来看看这些个方式都有什么: 

一、循环引用(Circular References) — IE浏览器的COM组件产生的对象实例和网页脚本引擎产生的对象实例相互引用,就会形成内存泄漏。 

这也是Web页面中咱们遇到的最多见和主要的泄漏方式; 

二、内部函数引用(Closures) — Closures能够当作是目前引发大量问题的循环应用的一种特殊形式。因为依赖指定的关键字和语法结构, 

Closures调用是比较容易被咱们发现的; 

三、页面交叉泄漏(Cross-Page Leaks) — 页面交叉泄漏实际上是一种较小的泄漏,它一般在你浏览过程当中,因为内部对象薄计引发。下面咱们 

会讨论DOM插入顺序的问题,在那个示例中你会发现只须要改动少许的代码,咱们就能够避免对象薄计对对象构建带来的影响; 

四、貌似泄漏(Pseudo-Leaks) — 这个不是真正的意义上的泄漏,不过若是你不了解它,你可能会在你的可用内存资源变得愈来愈少的时候极 

度郁闷。为了演示这个问题,咱们将经过重写Script元素中的内容来引起大量内存的"泄漏"。 

循环引用 

循环引用基本上是全部泄漏的始做俑者。一般状况下,脚本引擎经过垃圾收集器(GC)来处理循环引用,可是某些未知因数可能会妨碍从其 

环境中释放资源。对于IE来讲,某些DOM对象实例的状态是脚本没法得知的。下面是它们的基本原则: 

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

你可使用直接赋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文档卸载的时候统一清理,但大多数时候你可能会再造 

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

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

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

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

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

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

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元素插入顺序引发的问题那样。若是你以为使用所谓的"最佳"经验来编程,那么你就能够高枕无忧,可是这个示例让咱们看到,即便是"最佳"经验彷佛也可能带来泄漏。咱们这里的解决方案但愿能提升这些已有的好经验,或者介绍一些新经验使咱们避免泄漏发生的可能。

貌似泄漏(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属性引发的循环引用中,不过须要注意所注册的方法自身不要让泄漏(特别使用闭包的地方)跑掉。