权威JavaScript 中的内存泄露模式

作者:Abhijeet Bhattacharya (abhbhatt@in.ibm.com), 系统软件工程师, IBM India
Kiran Shivarama Shivarama Sundar (kisundar@in.ibm.com), 系统软件工程师, IBM India

2007 年 5 月 28 日

如果您知道内存泄漏的起因,那么在 JavaScript 中进行相应的防范就应该相当容易。在这篇文章中,作者 Kiran Sundar 和 Abhijeet Bhattacharya 将带您亲历 JavaScript 中的循环引用的全部基本知识,向您介绍为何它们会在某些浏览器中产生问题,尤其是在结合了闭包的情况下。在了解了您应该引起注意的常见内存泄漏模式之后,您还将学到应对这些泄漏的诸多方法。

JavaScript 是用来向 Web 页面添加动态内容的一种功能强大的脚本语言。它尤其特别有助于一些日常任务,比如验证密码和创建动态菜单组件。JavaScript 易学易用,但却很容易在某些浏览器中引起内存的泄漏。在这个介绍性的文章中,我们解释了 JavaScript 中的泄漏由何引起,展示了常见的内存泄漏模式,并介绍了如何应对它们。

注意本文假设您已经非常熟悉使用 JavaScript 和 DOM 元素来开发 Web 应用程序。本文尤其适合使用 JavaScript 进行 Web 应用程序开发的开发人员,也可供有兴趣创建 Web 应用程序的客户提供浏览器支持以及负责浏览器故障排除的人员参考。

JavaScript 中的内存泄漏

JavaScript 是一种垃圾收集式语言,这就是说,内存是根据对象的创建分配给该对象的,并会在没有对该对象的引用时由浏览器收回。JavaScript 的垃圾收集机制本身并没有问题,但浏览器在为 DOM 对象分配和恢复内存的方式上却有些出入。

Internet Explorer 和 Mozilla Firefox 均使用引用计数来为 DOM 对象处理内存。在引用计数系统,每个所引用的对象都会保留一个计数,以获悉有多少对象正在引用它。如果计数为零,该对象就会被销毁,其占用的内存也会返回给堆。虽然这种解决方案总的来说还算有效,但在循环引用方面却存在一些盲点。

循环引用的问题何在?

当两个对象互相引用时,就构成了循环引用,其中每个对象的引用计数值都被赋 1。在纯垃圾收集系统中,循环引用问题不大:若涉及到的两个对象中的一个对象被任何其他对象引用,那么这两个对象都将被垃圾收集。而在引用计数系统,这两个对象都不能被销毁,原因是引用计数永远不能为零。在同时使用了垃圾收集和引用计数的混合系统中,将会发生泄漏,因为系统不能正确识别循环引用。在这种情况下,DOM 对象和 JavaScript 对象均不能被销毁。清单 1 显示了在 JavaScript 对象和 DOM 对象间存在的一个循环引用。

清单 1. 循环引用导致了内存泄漏




<html>
<body>
<script type="text/javascript">
document.write("circular references between JavaScript and DOM!");
var obj;
window.onload = function(){
obj=document.getElementById("DivElement");
document.getElementById("DivElement").expandoProperty=obj;
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
};
</script>
<div id="DivElement">Div Element</div>
</body>
</html>

如上述清单中所示,JavaScript 对象 obj 拥有到 DOM 对象的引用,表示为 DivElement。而 DOM 对象则有到此 JavaScript 对象的引用,由 expandoProperty 表示。可见,JavaScript 对象和 DOM 对象间就产生了一个循环引用。由于 DOM 对象是通过引用计数管理的,所以两个对象将都不能销毁。

另一种内存泄漏模式

在清单 2 中,通过调用外部函数 myFunction 创建循环引用。同样,JavaScript 对象和 DOM 对象间的循环引用也会导致内存泄漏。

清单 2. 由外部函数调用引起的内存泄漏




<html>
<head>
<script type="text/javascript">
document.write(" object s between JavaScript and DOM!");
function myFunction(element)
{
this.elementReference = element;
// This code forms a circular reference here
//by DOM-->JS-->DOM
element.expandoProperty = this;
}
function Leak() {
//This code will leak
new myFunction(document.getElementById("myDiv"));
}
</script>
</head>
<body onload="Leak()">
<div id="myDiv"></div>
</body>
</html>

正如这两个代码示例所示,循环引用很容易创建。在 JavaScript 最为方便的编程结构之一:闭包中,循环引用尤其突出。

JavaScript 中的闭包

JavaScript 的过人之处在于它允许函数嵌套。一个嵌套的内部函数可以继承外部函数的参数和变量,并由该外部函数私有。清单 3 显示了内部函数的一个示例。

清单 3. 一个内部函数




function parentFunction(paramA)
{
var a = paramA;
function childFunction()
{
return a + 2;
}
return childFunction();
}

JavaScript 开发人员使用内部函数来在其他函数中集成小型的实用函数。如清单 3 所示,此内部函数 childFunction 可以访问外部函数 parentFunction 的变量。当内部函数获得和使用其外部函数的变量时,就称其为一个闭包

了解闭包

考虑如清单 4 所示的代码片段。

清单 4. 一个简单的闭包




<html>
<body>
<script type="text/javascript">
document.write("Closure Demo!!");
window.onload=
function closureDemoParentFunction(paramA)
{
var a = paramA;
return function closureDemoInnerFunction (paramB)
{
alert( a +" "+ paramB);
};
};
var x = closureDemoParentFunction("outer x");
x("inner x");
</script>
</body>
</html>

在上述清单中,closureDemoInnerFunction 是在父函数 closureDemoParentFunction 中定义的内部函数。当用外部的 xclosureDemoParentFunction 进行调用时,外部函数变量 a 就会被赋值为外部的 x。函数会返回指向内部函数 closureDemoInnerFunction 的指针,该指针包括在变量 x 内。

外部函数 closureDemoParentFunction 的本地变量 a 即使在外部函数返回时仍会存在。这一点不同于 C/C++ 这样的编程语言,在 C/C++ 中,一旦函数返回,本地变量也将不复存在。在 JavaScript 中,在调用 closureDemoParentFunction 的时候,带有属性 a 的范围对象将会被创建。该属性包括值 paramA,又称为“外部 x”。同样地,当 closureDemoParentFunction 返回时,它将会返回内部函数 closureDemoInnerFunction,该函数包括在变量 x 中。

由于内部函数持有到外部函数的变量的引用,所以这个带属性 a 的范围对象将不会被垃圾收集。当对具有参数值 inner xx 进行调用时,即 x("inner x"),将会弹出警告消息,表明 “outer x innerx”。

清单 4 简要解释了 JavaScript 闭包。闭包功能非常强大,原因是它们使内部函数在外部函数返回时也仍然可以保留对此外部函数的变量的访问。不幸的是,闭包非常易于隐藏 JavaScript 对象 和 DOM 对象间的循环引用。

闭包和循环引用

在清单 5 中,可以看到一个闭包,在此闭包内,JavaScript 对象(obj)包含到 DOM 对象的引用(通过 id "element" 被引用)。而 DOM 元素则拥有到 JavaScript obj 的引用。这样建立起来的 JavaScript 对象和 DOM 对象间的循环引用将会导致内存泄漏。

清单 5. 由事件处理引起的内存泄漏模式




<html>
<body>
<script type="text/javascript">
document.write("Program to illustrate memory leak via closure");
window.onload=function outerFunction(){
var obj = document.getElementById("element");
obj.onclick=function innerFunction(){
alert("Hi! I will leak");
};
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
// This is used to make the leak significant
};
</script>
<button id="element">Click Me</button>
</body>
</html>

避免内存泄漏

幸好,JavaScript 中的内存泄漏是可以避免的。当确定了可导致循环引用的模式之后,正如我们在上述章节中所做的那样,您就可以开始着手应对这些模式了。这里,我们将以上述的 由事件处理引起的内存泄漏模式 为例来展示三种应对已知内存泄漏的方式。

一种应对 清单 5 中的内存泄漏的解决方案是让此 JavaScript 对象 obj 为空,这会显式地打破此循环引用,如清单 6 所示。

清单 6. 打破循环引用




<html>
<body>
<script type="text/javascript">
document.write("Avoiding memory leak via closure by breaking the circular
reference");
window.onload=function outerFunction(){
var obj = document.getElementById("element");
obj.onclick=function innerFunction()
{
alert("Hi! I have avoided the leak");
// Some logic here
};
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
obj = null; //This breaks the circular reference
};
</script>
<button id="element">"Click Here"</button>
</body>
</html>

清单 7 是通过添加另一个闭包来避免 JavaScript 对象和 DOM 对象间的循环引用。

清单 7. 添加另一个闭包




<html>
<body>
<script type="text/javascript">
document.write("Avoiding a memory leak by adding another closure");
window.onload=function outerFunction(){
var anotherObj = function innerFunction()
{
// Some logic here
alert("Hi! I have avoided the leak");
};
(function anotherInnerFunction(){
var obj = document.getElementById("element");
obj.onclick=anotherObj })();
};
</script>
<button id="element">"Click Here"</button>
</body>
</html>

清单 8 则通过添加另一个函数来避免闭包本身,进而阻止了泄漏。

清单 8. 避免闭包自身




<html>
<head>
<script type="text/javascript">
document.write("Avoid leaks by avoiding closures!");
window.onload=function()
{
var obj = document.getElementById("element");
obj.onclick = doesNotLeak;
}
function doesNotLeak()
{
//Your Logic here
alert("Hi! I have avoided the leak");
}

</script>
</head>
<body>
<button id="element">"Click Here"</button>
</body>
</html>


结束语

本文解释了循环引用是如何导致 JavaScript 中的内存泄漏的 —— 尤其是在结合了闭包的情况下。您还了解了涉及到循环引用的一些常见内存泄漏模式以及应对这些泄漏模式的几种简单方式。

作者简介









Abhijeet Bhattacharya 是 IBM 印度软件实验室的一名系统工程师。在过去三年中,他一直是 OS/2 IBM Web Browser 支持团队中的一员。他也具有系统管理领域的相关经验,并参与过 IBM Pegasus 开源创新项目。他目前工作的重点包括分布式计算和 SARPC。他拥有 Rajiv Gandhi Technical University 的工程学士学位。










Kiran Shivarama Sundar 是 IBM 印度软件实验室的一名系统工程师。在过去三年中,他一直是 OS/2 IBM Web Browser 支持团队中的一员。他同时也具有诸多其他项目的工作经验,包括为 Apache Tuscany Open Source Project 开发命令行工具以及为 IBM 的 EPCIS 团队开发 RFIDIC Installer。目前,Kiran 加入了 IBM WebSphere Adapters 支持团队,负责提供对 JMS 和 MQ 适配器的支持。他已成功获得了 Sun Certified Java Programmer、Sun Certified Web Component Developer 和 Sun Certified Business Component Developer 的认证。他目前所关注的领域包括 Java、J2EE、Web 服务和 SOA。他拥有 Visweshwaraya Technology University 的工程学士学位。

(0)

相关推荐

  • js内存泄露的几种情况详细探讨

    内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束.在C++中,因为是手动管理内存,内存泄露是经常出现的事情.而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露.浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露. 1.当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露. 复制代码 代码如下: <div id="

  • jQuery内存泄露解决办法

    本文大家分享了jQuery内存泄露解决办法,供大家参考,具体内容如下 思路:为JQuery扩展删除jquery元素对象的方法,大大减轻内存泄露的压力 ;(function($){ if(!$.lui.widget) $.lui.widget = {}; //$.lui.newGuid()生成随机32位id //如果采用此方式多次生成jquery对象的话,html代码字符串会在内存中多次重复,占用额外的内容,也会有泄露.而$("<span></span>").at

  • JQuery1.4+ Ajax IE8 内存泄漏问题

    并且JQuery1.3.2版本不存在,JQuery1.4+版本存在 通过对比,最终找到这两个版本的其中一个差别 JQuery1.4+在ajax方法增加了一段代码,用于在abort时调用onreadystatechange方法 复制代码 代码如下: try { var oldAbort = xhr.abort; xhr.abort = function() { if ( xhr ) { oldAbort.call( xhr ); } onreadystatechange(); }; } catch

  • 使用jQuery Ajax功能时需要注意的一个问题(内存溢出)

    最近一哥们在做一个Ajax长连接的项目,页面需要和服务器保持长连接,而且在连接超时后需要重新请求连接,过程中他问我要用到什么,我也是想都没想就告诉他用jQuery.jQuery不是有ajaxSuccess ajaxError这些对象吗,在请求完成或者请求失败后重新请求不就好了. 但是后来他告诉我说没有用 jQuery,自己手工写的XMLhttprequest .他告诉我说,开始是用jquery写的,而且在测试过程中也没有出现问题.但是在后来无意中发现,在页面开的时候久了之后,浏览器资源竟然占用非

  • jquery不会自动回收xmlHttpRequest对象 导致了内存溢出

    之前一直都不曾注意到这一点,今天幸亏看了kuibono的文章,下面是kuibono给出的手动回收xmlHttpRequest对象的代码片断: 每次jquery的Ajax请求都会创建一个xmlHttprequest对象,理论上讲,长连接的请求是一个无限递归,请求数量是非常大的,但是由于每次请求都会建立一个新的xmlhttprequest,而且jquery不会自动回收资源,所以导致了内存溢出. 通过查看jquery API,发现jquery还有一个 complete对象,是请求完成后回调函数 (请求

  • Javascript 闭包引起的IE内存泄露分析

    复制代码 代码如下: function fors(){ obj_a = obj_b; obj_b.attr = obj_a; } 复制代码 代码如下: function fors(){ obj_b = {}; obj_b.attr = obj_b; } 上面是两个个很显示的循环引用,IE中产生了内存泄露,由于IE的内存回收机制,导至会长期占用内存而不能释放. 但闭包的内存泄露,有些隐蔽.因为闭包的循环引用,是间接的. 复制代码 代码如下: function iememery(){ var js_

  • JQuery Dialog的内存泄露问题解决方法

    对于页面来说,JQuery中的Dialog从效果上来说还可以,而且使用简单,只要短短几行绑定的代码就可以实现弹出效果. 代码 复制代码 代码如下: $('#dialog').dialog({ autoOpen: false, width: 600, buttons: { "Ok": function() { $(this).dialog("close"); }, "Cancel": function() { $(this).dialog(&quo

  • IE下使用jQuery重置iframe地址时内存泄露问题解决办法

    页面中有个iframe: 复制代码 代码如下: <iframe src='a.html'></iframe> <button>测试IFRAME泄露</button> 其中a.html内容如下: 复制代码 代码如下: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</t

  • 容易造成JavaScript内存泄露几个方面

    发表于谷歌WebPerf(伦敦WebPerf集团),​​2014年8月26日. 高效的JavaScript Web应用必须流畅,快速.与用户交互的任何应用程序,都需要考虑如何确保内存有效使用,因为如果消耗过多,页面就会崩溃,迫使用户重新加载.而你只能躲在角落哭泣. 自动垃圾收集是不能代替有效的内存管理的,特别是在大型,长时间运行的Web应用程序中.在这次讲座中,我们将演示如何通过Chrome的DevTools对内存进行有效的管理. 并了解如何解决性能问题,如内存泄漏,频繁的垃圾收集暂停,和整体内

  • 关于js内存泄露的一个好例子

    我把别人的例子改了一下,觉得这样写更紧凑!套用别人的原话,当一个DOM对象包含一个Js对象的引用(例如一个Event Handler), 而这个Js对象又持有对这个DOM对象的引用时,一个环状引用就行成了,于是在ie下就出现了内存泄露.点击"运行代码"并打开任务管理器看内存变化.分别在ie8和ff下测试,差距不用多说. 运行代码 复制代码 代码如下: <html>  <head>    <title>Memory leak</title>

随机推荐