内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。
泄露方式
在接下来的内容中,我们会讨论内存泄露方式,并为每种方式给出示例。其中一个重要的示例是JScript中的Closure技术,另一个示例是在事件执行 中使用Closures。当你熟悉本示例后,你就能找出并修改你已有的大多数内存泄漏问题,但是其它Closure相关的问题可能又会被忽视。
现在让我们来看看这些个方式都有什么:
1、循环引用(Circular References) — IE浏览器的COM组件产生的对象实例和网页脚本引擎产生的对象实例相互引用,就会造成内存泄漏。这也是Web页面中我们遇到的最常见和主要的泄漏方式;
2、内部函数引用(Closures) — Closures可以看成是目前引起大量问题的循环应用的一种特殊形式。由于依赖指定的关键字和语法结构,Closures调用是比较容易被我们发现的;
3、页面交叉泄漏(Cross-Page Leaks) — 页面交叉泄漏其实是一种较小的泄漏,它通常在你浏览过程中,由于内部对象薄计引起。下面我们会讨论DOM插入顺序的问题,在那个示例中你会发现只需要改动 少量的代码,我们就可以避免对象薄计对对象构建带来的影响;
4、貌似泄漏(Pseudo-Leaks) — 这个不是真正的意义上的泄漏,不过如果你不了解它,你可能会在你的可用内存资源变得越来越少的时候极度郁闷。为了演示这个问题,我们将通过重写Script元素中的内容来引发大量内存的"泄漏"。
1.当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露。
<div id="myDiv"> <input type="button" value="Click me" id="myBtn"> </div> <script type="text/javascript"> var btn = document.getElementById("myBtn"); btn.onclick = function(){ document.getElementById("myDiv").innerHTML = "Processing..."; } </script>
应改成下面
<div id="myDiv"> <input type="button" value="Click me" id="myBtn"> </div> <script type="text/javascript"> var btn = document.getElementById("myBtn"); btn.onclick = function(){ btn.onclick = null; document.getElementById("myDiv").innerHTML = "Processing..."; } </script>
或者采用事件委托
<div id="myDiv"> <input type="button" value="Click me" id="myBtn"> </div> <script type="text/javascript"> document.onclick = function(event){ event = event || window.event; if(event.target.id == "myBtn"){ document.getElementById("myDiv").innerHTML = "Processing..."; } } </script>
2.循环引用
三个对象 A 、B 、C
AàBàC :A的某一属性引用着B,同样C也被B的属性引用着。如果将A清除,那么B、C也被释放。
AàBàCàB :这里增加了C的某一属性引用B对象,如果这是清除A,那么B、C不会被释放,因为B和C之间产生了循环引用。
例1:
var a=document.getElementById("#xx"); var b=document.getElementById("#xxx"); a.r=b; b.r=a;
对于纯粹的 ECMAScript 对象而言,只要没有其他对象引用对象 a、b,也就是说它们只是相互之间的引用,那么仍然会被垃圾收集系统识别并处理。但是,在 Internet Explorer 中,如果循环引用中的任何对象是 DOM 节点或者 ActiveX 对象,垃圾收集系统则不会发现它们之间的循环关系与系统中的其他对象是隔离的并释放它们。最终它们将被保留在内存中,直到浏览器关闭。
例2:
var a = {}; a.pro = { a:100 }; a.pro.pro = { b:100 }; a = null ; //这种情况下,{a:100}和{b:100}就同时也被释放了。 var obj = {}; obj.pro = { a : 100 }; obj.pro.pro = { b : 200 }; var two = obj.pro.pro; obj = null; //这种情况下 {b:200}不会被释放掉,而{a:100}被释放了。
3.在事件中绑定匿名函数
var elem = document.getElementById('test'); elem.addEventListener('click', function() { alert('You clicked ' + elem.tagName); });
这段代码把一个匿名函数注册为一个DOM结点的click事件处理函数,函数内引用了一个DOM对象elem,就形成了闭包。这就会产生一个循环引用,即:DOM->闭包->DOM->闭包...DOM对象在闭包释放之前不会被释放;而闭包作为DOM对象的事件处理函数存在,所以在DOM对象释放前闭包不会释放,即使DOM对象在DOM tree中删除,由于这个循环引用的存在,DOM对象和闭包都不会被释放。可以用下面的方法可以避免这种内存泄露
4.闭包
function bindEvent() { var obj=document.createElement("XXX"); obj.onclick=function(){ //Even if it's a empty function } }
闭包非常容易构成循环引用。如果一个构成闭包的函数对象被指定给,比如一个 DOM 节点的事件处理器,而对该节点的引用又被指定给函数对象作用域中的一个活动(或可变)对象,那么就存在一个循环引用。
DOM_Node.onevent -<function_object.[[scope]] -<scope_chain -<Activation_object.nodeRef -<DOM_Node。
形成这样一个循环引用是轻而易举的,而且稍微浏览一下包含类似循环引用代码的网站(通常会出现在网站的每个页面中),就会消耗大量(甚至全部)系统内存。
解决之道,将事件处理函数定义在外部,解除闭包
正确代码:
function bindEvent() { var obj=document.createElement("XXX"); obj.onclick=onclickHandler; } function onclickHandler(){ //do something }
或者在定义事件处理函数的外部函数中,删除对dom的引用(题外,《JavaScript权威指南》中介绍过,闭包中,作用域中没用的属性可以删除,以减少内存消耗。)
function bindEvent() { var obj=document.createElement("XXX"); obj.onclick=function(){ //Even if it's a empty function } obj=null; }
5.删除delete
a = {p: {x: 1}}; b = a.p; delete a.p;
执行这段代码之后b.x的值依然是1.由于已经删除的属性引用依然存在,因此在JavaScript的某些实现中,可能因为这种不严谨的代码而造成内存泄露。所以在销毁对象的时候,要遍历属性中属性,依次删除。
6.自动类型装箱转换
别不相信,下面的代码在ie系列中会导致内存泄露
var s=”lalala”; alert(s.length);
s本身是一个string而非object,它没有length属性,所以当访问length时,JS引擎会自动创建一个临时String对象封装s,而这个对象一定会泄露。这个bug匪夷所思,所幸解决起来相当容易,记得所有值类型做.运算之前先显式转换一下:
var s="lalala"; alert(new String(s).length);
7.某些DOM操作
IE系列的特有问题 简单的来说就是在向不在DOM树上的DOM元素appendChild;IE7中,貌似为了改善内存泄露,IE7采用了极端的解决方案:离开页面时回收所有DOM树上的元素,其它一概不管。
javascript具有自动垃圾收集机制
标记清除
javascript中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占的内存,因为只要执行流进入相应的环境,就可能用到它们。而当变量离开环境时,这将其标记为“离开环境”。
可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键采取什么策略。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中变量以及被环境中的变量引用的变量标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
到2008年为止,IE、Firefox、Opera、Chrome和Safari的javascript实现使用的都是标记清除式的垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。
引用计数
另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数。当声明一个变量并将引用类型的值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得另外一个值,则这个值的引用次数减1.当这个值的引用次数变成0时,则说明没有办法访问这个值了,因此就可以将其占用的内存空间回收回来。这样当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
Netscape Navigator 3.0是最早使用引用计数策略的浏览器,但很快它就遇到了一个严重的问题:循环引用。循环引用指的是对象A中包含一个指向对象B的引用,而对象B中也包含一个指向对象A的引用。
IE中有一部分对象并不是原生javascript对象。例如,其中BOM和DOM中的对象就是使用C++以COM (Component Object Model,组件对象模型)对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略。因此,即使IE的javascript引擎是使用标记清除策略来实现的,但javascript访问的COM对象依然是基于引用计数策略的。换句话说,只要IE中设计COM对象,就会存在循环引用的问题。
下面这个简单的例子,展示了使用COM对象导致的循环引用问题:
复制代码代码如下:
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.somObject = myObject;
这里例子在一个DOM元素(element)与一个原生的javascript对象(myObject)之间创建了循环引用。其中,变量myObject 有一个名为element的属性指向element对象;而变量element也有一个属性名叫someObject回指myObject。由于存在这个循环引用,即使将例子中的DOM从页面中移除,它也永远不会被回收。
为了避免类似这样的循环引用问题,最好是不使用他们的时候手工断开原生javascript对象与DOM元素之间的连接。例如,可以使用下面的代码消除前面例子创建的循环引用:
复制代码代码如下:
myObject.element = null;
element.somObject = null;
将变量设置为null,意味着切断变量与它此前引用的值之间的连接。但垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
《JavaScript and memory leaks》这篇文章介绍了一些JavaScript内存泄漏的例子:
http://www.javascriptkit.com/javatutors/closuresleak/index.shtml
JavaScript内存泄漏检查工具:
1、JavaScript Memory Leak Detector
http://blogs.msdn.com/gpde/pages/javascript-memory-leak-detector_v2.aspx
下载地址:
http://www.mseurojobs.members.winisp.net/JSLeaksDetector.msi.zip
http://joinmicrosofteurope.com/files/IEJSLeaksDetector2.0.1.1.zip
2、sIEve
工具主页:
http://home.orange.nl/jsrosman/
下载地址:
http://home.orange.nl/jsrosman/sIEve-0.0.8.exe
原文链接