zl程序教程

您现在的位置是:首页 >  前端

当前栏目

权威JavaScript中的内存泄露模式

JavaScript内存模式 泄露 权威
2023-06-13 09:13:57 时间

作者:
AbhijeetBhattacharya(abhbhatt@in.ibm.com),系统软件工程师,IBMIndia
KiranShivaramaShivaramaSundar(kisundar@in.ibm.com),系统软件工程师,IBMIndia

2007年5月28日

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

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

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

JavaScript中的内存泄漏

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

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

循环引用的问题何在?

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

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

<html> <body> <scripttype="text/javascript"> document.write("circularreferencesbetweenJavaScriptandDOM!"); varobj; window.onload=function(){ obj=document.getElementById("DivElement"); document.getElementById("DivElement").expandoProperty=obj; obj.bigString=newArray(1000).join(newArray(2000).join("XXXXX")); }; </script> <divid="DivElement">DivElement</div> </body> </html>
如上述清单中所示,JavaScript对象obj拥有到DOM对象的引用,表示为DivElement。而DOM对象则有到此JavaScript对象的引用,由expandoProperty表示。可见,JavaScript对象和DOM对象间就产生了一个循环引用。由于DOM对象是通过引用计数管理的,所以两个对象将都不能销毁。

另一种内存泄漏模式

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

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

<html> <head> <scripttype="text/javascript"> document.write("objectsbetweenJavaScriptandDOM!"); functionmyFunction(element) { this.elementReference=element; //Thiscodeformsacircularreferencehere //byDOM-->JS-->DOM element.expandoProperty=this; } functionLeak(){ //Thiscodewillleak newmyFunction(document.getElementById("myDiv")); } </script> </head> <bodyonload="Leak()"> <divid="myDiv"></div> </body> </html>
正如这两个代码示例所示,循环引用很容易创建。在JavaScript最为方便的编程结构之一:闭包中,循环引用尤其突出。

JavaScript中的闭包

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


清单3.一个内部函数

functionparentFunction(paramA) { vara=paramA; functionchildFunction() { returna+2; } returnchildFunction(); }
JavaScript开发人员使用内部函数来在其他函数中集成小型的实用函数。如清单3所示,此内部函数childFunction可以访问外部函数parentFunction的变量。当内部函数获得和使用其外部函数的变量时,就称其为一个闭包

了解闭包

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


清单4.一个简单的闭包

<html> <body> <scripttype="text/javascript"> document.write("ClosureDemo!!"); window.onload= functionclosureDemoParentFunction(paramA) { vara=paramA; returnfunctionclosureDemoInnerFunction(paramB) { alert(a+""+paramB); }; }; varx=closureDemoParentFunction("outerx"); x("innerx"); </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的范围对象将不会被垃圾收集。当对具有参数值innerxx进行调用时,即x("innerx"),将会弹出警告消息,表明“outerxinnerx”。

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

闭包和循环引用

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


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

<html> <body> <scripttype="text/javascript"> document.write("Programtoillustratememoryleakviaclosure"); window.onload=functionouterFunction(){ varobj=document.getElementById("element"); obj.onclick=functioninnerFunction(){ alert("Hi!Iwillleak"); }; obj.bigString=newArray(1000).join(newArray(2000).join("XXXXX")); //Thisisusedtomaketheleaksignificant }; </script> <buttonid="element">ClickMe</button> </body> </html>
避免内存泄漏

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

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

 清单6.打破循环引用

<html> <body> <scripttype="text/javascript"> document.write("Avoidingmemoryleakviaclosurebybreakingthecircular reference"); window.onload=functionouterFunction(){ varobj=document.getElementById("element"); obj.onclick=functioninnerFunction() { alert("Hi!Ihaveavoidedtheleak"); //Somelogichere }; obj.bigString=newArray(1000).join(newArray(2000).join("XXXXX")); obj=null;//Thisbreaksthecircularreference }; </script> <buttonid="element">"ClickHere"</button> </body> </html>
清单7是通过添加另一个闭包来避免JavaScript对象和DOM对象间的循环引用。

清单7.添加另一个闭包

<html> <body> <scripttype="text/javascript"> document.write("Avoidingamemoryleakbyaddinganotherclosure"); window.onload=functionouterFunction(){ varanotherObj=functioninnerFunction() { //Somelogichere alert("Hi!Ihaveavoidedtheleak"); }; (functionanotherInnerFunction(){ varobj=document.getElementById("element"); obj.onclick=anotherObj})(); }; </script> <buttonid="element">"ClickHere"</button> </body> </html>
清单8则通过添加另一个函数来避免闭包本身,进而阻止了泄漏。

清单8.避免闭包自身

<html> <head> <scripttype="text/javascript"> document.write("Avoidleaksbyavoidingclosures!"); window.onload=function() { varobj=document.getElementById("element"); obj.onclick=doesNotLeak; } functiondoesNotLeak() { //YourLogichere alert("Hi!Ihaveavoidedtheleak"); } </script> </head> <body> <buttonid="element">"ClickHere"</button> </body> </html>
结束语

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

作者简介

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


KiranShivaramaSundar是IBM印度软件实验室的一名系统工程师。在过去三年中,他一直是OS/2IBMWebBrowser支持团队中的一员。他同时也具有诸多其他项目的工作经验,包括为ApacheTuscanyOpenSourceProject开发命令行工具以及为IBM的EPCIS团队开发RFIDICInstaller。目前,Kiran加入了IBMWebSphereAdapters支持团队,负责提供对JMS和MQ适配器的支持。他已成功获得了SunCertifiedJavaProgrammer、SunCertifiedWebComponentDeveloper和SunCertifiedBusinessComponentDeveloper的认证。他目前所关注的领域包括Java、J2EE、Web服务和SOA。他拥有VisweshwarayaTechnologyUniversity的工程学士学位。