避免浏览器内存泄露
最近做RIA,因为涉及到大数据量处理,担心出现内存泄漏,于是花了一段时间,从了解到处理,有些心得,在这简单做个总结。
内存泄漏通俗的说:使用者认为是垃圾,而浏览器认为不是,出现了误会,资源便浪费了。而数据是否被引用是系统判断是否垃圾的依据,所以一切引起垃圾的根源就是错误的引用,解决JavaScript内存泄漏问题的根本就是避免错误引用。
内存泄漏方式我分成三类:
- 公共变量未释放
- 循环引用
- 内部泄漏
以下代码是测试的基本代码,有两个个要点。一个是建立大数据的对象,另外一个是利用IE提供的程序回收内存方法,设定浏览器每秒中做一次垃圾回收。如:
- <p><font size="3"><font color="#000000">
- 程序清单1:
- <HTML>
- <HEAD>
- <TITLE> 测试内存泄漏 </TITLE>
- <SCRIPT LANGUAGE="JavaScript">
- <!--
- //创建占用内存的对象.
- //疑惑不解,这段代码建立5000*100的数组,Number占用8字节,算下来也就4M,IE却占了几十M
- function makeObject(){
- var arr = []
- for (var i=0;i<5000 ;i++ ){
- arr[i] = []
- for (var j=0;j<100 ;j++ ){
- arr[i][j] = j;
- }
- }
- return arr;
- }
- //每秒中做一次垃圾回收.IE可控制回收,FF过几十秒自动回收一次,不用调度,看效果要等
- window.setInterval(function gc(){
- if(document.all) CollectGarbage();
- }, 1000);
- //-->
- </SCRIPT>
- </HEAD>
- <BODY></font></font></p><p><font size="3"><font color="#000000"> <!-- 这里插入测试代码 -->
- </BODY>
- </HTML>
- </font></font></p>
下面用到的所有测试代码只要放在代码清单1<Body></Body>中,打开一个IE窗口,使用任务管理器,比较操作前后对应IE进程占用的内存变化情况(最好在“选择列”中增加“虚拟内存”列,可以看得更清楚),即可看到代码是否会泄漏内存。
1. 公共变量引发内存泄漏
声明一下,严格的说,公共变量引发的泄漏并不是真正的内存泄漏,毕竟代码是可控制回收的。但对于使用者而言,这部分内存不可用,从这个角度而言,算是应用层的泄漏吧。
公共变量是最容易出现,而又最容易避免的泄漏,看个例子,把下面代码放在程序清单1的Body中:
- <font color="#000000" size="3">程序清单2:
- <script language="javascript">
- var pubarr;
- function regMem(){
- pubarr = makeObject();
- }
- function removeMem(){
- pubarr = null;
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="regMem()" title="一按我就长">
- <INPUT TYPE="button" VALUE="删除对象" ONCLICK="removeMem()" title="一按我就小">
- </font>
按下【创建对象】,内存就增长,并且不能自动释放。按下【删除对象】,内存就回复到原水平。当然大多数开发者不会用公用变量传递参数。许多错误都在不经意中出现:
- <font size="3"><font color="#000000">程序清单3:
- <script language="javascript">
- function regMem(){
- localarr = makeObject();
- }
- function removeMem(){
- localarr = null;
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="regMem()" title="一按我就长">
- <INPUT TYPE="button" VALUE="删除对象" ONCLICK="removeMem()" title="一按我就小"></font></font>
在上面regMem方法中,localarr定义漏了var声明,结果localarr就成为了全局变量。删除上一行,改用var localarr = makeObject()在怎么创建对象,内存都可以在一瞬间给回收,如:
- <font size="3"><font color="#000000">程序清单4:
- <script language="javascript">
- function regMem(){
- var localarr = makeObject();
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="regMem()" title="怎么来都不怕"></font></font>
通过这个例子也顺便可修正一个误区,有人说所有变量使用完都必须将其设置成null,否则就可能无法释放内存。其实这是对Javascript内存释放的一个误解,真正可以释放的对象是没有被引用的对象。
还有一种公共变量造成内存泄漏的可能是我们在使用中通常无意中写错名字,感觉程序运行正常,但是却无意造成了泄漏:
- <font size="3"><font color="#000000">程序清单5:
- <script language="javascript">
- function regMem(){
- var localArr
- localarr = makeObject();
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="regMem()" title="一按我就长"></font></font>
上面代码localArr和locallarr一字之差,内存泄漏了。
公共变量除了全局变量外,还有别的形式,如黏附在DOM对象上的自定义属性。
- <font size="3"><font color="#000000">程序清单6:
- <script language="javascript">
- function leak(){
- var localarr = makeObject();
- var ele = document.createElement("div");
- document.getElementById("myDiv").appendChild(ele);
- //对象黏附在ele中
- ele.arr = localarr
- ele = null;
- localarr = null;
- }
- //删除属性的释放方法
- function breakLeak(){
- document.getElementById("myDiv").firstChild.arr = null;
- }
- //清除对象的释放方法
- function freeLeak(){
- var ele = document.getElementById("myDiv");
- ele.removeChild(ele.firstChild); //或ele.innerHTML = ""
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="leak()" title="一按我就长">
- <INPUT TYPE="button" VALUE="删除黏贴" ONCLICK="breakLeak()" title="一按我就小">
- <INPUT TYPE="button" VALUE="清除对象" ONCLICK="freeLeak()" title="释放对象">
- <div id="myDiv"></div></font></font>
黏附在DOM对象的属性在不使用时,可将其设置成null或使用delete方法将其删除。释放节点后,黏附对象也可被回收。
避免使用公共使用的变量造成内存泄漏很容易,需要有良好的编程习惯:
1) 尽量避免使用公用变量,函数调用使用参数传递需要的内容
2) 而变量定义一定要使用var
3) 变量大小写规定一致,除第一个字母外,其他单词第一字母都用大写
4) 如不可避免使用到公共变量,使用完成后需将其设置成null或使用delete方法将其从对象属性中删除
即使是再小心,也容易出意外的。只能在运行时检查,使用Firefox的FireBug插件可以看到页面的公用变量定义,IE可使用Script Debug工具,也可看到变量定义情况。看到异常的变量,就是泄漏了。
在进入循环引用一节前,我们首先看一段代码:
- <font size="3"><font color="#000000">程序清单7:
- <script language="javascript">
- function leak(){
- var localarr = makeObject();
- var localobj = {};
- localarr.obj = localobj
- localobj.arr = localarr
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="leak()" title="按了也白按">
- </font></font>
以上代码中,两个本地对象形成了相互引用,但在函数退出后,却没有占用浏览器任何内存。在过程调用完成后,两个本地变量引用被置空,虽然相互引用对方,但确可以被释放。但这一点也不奇快,作为整体,没有外部变量引用他们,因此,被系统作为整体释放掉了。这给我们一个有益的提示,就像把风筝的线割断,风筝再复杂,一切都会回归到零。
2. 循环引用
说起循环引用造成内存泄漏,基本上都和DOM对象有关。而Javascript对象在这里只是配角。我们在程序清单6的基础上做个改造,增加了一个本地对象,造成循环引用。
- <font size="3"><font color="#000000">程序清单8:
- <script language="javascript">
- function leak(){
- var localarr = makeObject();
- var ele = document.createElement("div");
- document.getElementById("myDiv").appendChild(ele);
- //对象黏附在ele中
- ele.arr = localarr
- localarr.ele = ele //增加了此行
- ele = null;
- localarr = null;
- }
- //删除属性的释放方法
- function breakLeak(){
- document.getElementById("myDiv").firstChild.arr = null;
- }
- //清除对象的释放方法失效了
- function freeLeak(){
- var ele = document.getElementById("myDiv");
- ele.removeChild(ele.firstChild); //或ele.innerHTML = ""
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="leak()" title="一按我就长">
- <INPUT TYPE="button" VALUE="删除黏贴" ONCLICK="breakLeak()" title="一按我就小">
- <INPUT TYPE="button" VALUE="清除对象" ONCLICK="freeLeak()" title="此方法释放失败">
- <div id="myDiv"></div>
- </font></font>
以上代码比起前代码清单6只是增加了一行,但是清除DOM对象的方法失效了。也就是说即使删除了DOM对象,其循环引用的属性并不能自动被清除,造成了内存泄漏。更为不幸的是,即使刷新这个网页,甚至是关闭这个Tab(浏览器支持多Tab),内存也不能回来。
使用属性造成循环的可能性一般比较少,不是特别拙劣的设计不会使用到。然而,使用过程(function)造成的循环引用在面向对象的封装中就比比皆是了,这就是传说中的闭包(closure)。闭包简而言之就是方法中包含方法,闭包本身不会造成内存泄漏,关键是闭包与DOM对象之间的引用。
- <font size="3"><font color="#000000"><b style="mso-bidi-font-weight: normal">程序清单9:
- <script language="javascript">
- function leak(){
- var container = document.getElementById("myDiv");
- var ele = container.firstChild;
- if(ele!=null) {
- ele.onclick = null;
- container.removeChild(ele);
- }
- ele = document.createElement("div");
- ele.innerHTML = "Click me!";
- ele.onclick = myClick;
- container.appendChild(ele);
- var arr = makeObject();
- function myClick(){ //闭包. leak包含了myClick
- alert("Hello world!");
- }
- }
- function breakLeak(){
- var ele = document.getElementById("myDiv");
- ele.firstChild.onclick = hello;
- }
- function freeLeak(){
- var ele = document.getElementById("myDiv");
- ele.removeChild(ele.firstChild);
- ele.innerHTML = "";
- }
- function hello(){
- alert("Hello Jimmy!");
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="leak()" title="一按我就长">
- <INPUT TYPE="button" VALUE="成功释放" ONCLICK="breakLeak()" title="释放方法即可">
- <INPUT TYPE="button" VALUE="错误释放" ONCLICK="freeLeak()" title="释放对象">
- <div id="myDiv"></div>
- </b></font></font>
程序清单9中,方法leak引用内部方法myClick,DOM对象也引用了myClick,也许就这样,在浏览器释放DOM对象时,无法判断是否可释放DOM,即使调用结束了,过程中使用的对象无法被释放,造成了内存泄漏。是否是DOM释放的代码问题,还是别的什么原因,也许只有微软的工程师才知道了。
堵住漏洞,只有破坏他的循环引用。像在程序清单8和程序清单9中breakLeak方法一样,只要在形成循环引用的任意一个环节破坏它,让循环引用不存在即可。在循环引用中删除DOM对象是不可行的方法,只会造成内存永远也无法回收,至少在IE7以下的版本是这样的。
看了IE那么多问题,难道真要删除每个DOM对象都要清除其属性和事件吗?
- <font size="3"><font color="#000000">程序清单10:
- <script language="javascript">
- function Closure(){
- var arr = makeObject();
- var container = document.getElementById("myDiv");
- function myClick(){
- alert("Hello world!");
- }
- this.attatchEvent = function(){
- var ele = container.firstChild;
- if(ele!=null) {
- ele.onclick = null;
- container.removeChild(ele);
- }
- ele = document.createElement("div");
- ele.innerHTML = "Click me!";
- ele.onclick = myClick; //增加了内部方法作为事件
- ele.arr = arr; //增加了内部属性作为DOM对象的属性
- container.appendChild(ele);
- }
- this.free = function(){
- container.innerHTML = ""; //直接释放DOM对象
- }
- }
- var cls;
- function createLeak(){
- cls = new Closure();
- cls.attatchEvent();
- }
- function freeLeak(){
- cls.free();
- cls = null;
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="createLeak()" title="创建对象">
- <INPUT TYPE="button" VALUE="释放对象" ONCLICK="freeLeak()" title="Oh, yeah!!">
- <div id="myDiv"></div>
- </font></font>
以上的代码没有发现内存泄漏,Oh my god!写到这,几乎推翻了循环引用对闭包所做的结论,对DOM对象的引用有了新的看法。难道问题不在DOM对象?难道是过程调用出了问题?我不知道,我也不想知道,至少在写下一节前我还想保持好的心情。Javascript使用function关键字作为对象封装时,把function看作是一个代名词,也许可以换个名字class,也许这样会好理解些。
解决了开发过程的大问题,但也不能高兴太早,即使是使用了对象封装,程序清单8说描述的问题仍然不能避免。
总结一下循环引用造成的泄漏
1) 循环引用和DOM相关
2) 引用的内容可以是Javascript的属性、方法,也可以是DOM对象到DOM对象的引用
3) 删除循环引用的任意环节引用关系,可避免内存泄漏。删除对应的DOM对象不能解决循环引用的泄漏问题
对开发者而言,没有什么东西比习惯更重要了:
1) 避免循环引用,特别是与DOM对象关联的循环引用
2) 在Javascript开发中,尽可能使用面向对象的设计代替面向过程的设计。什么?不会?赶紧来吧朋友,面向对象的海洋很广阔……
3. 内部泄漏
内部泄露理应问题本不存在,或者说不具有普遍性,也许只是个别代码,个别浏览器特殊的写法造成的。在希望浏览器升级之余,我们也要避免问题发生。
- <font size="3"><font color="#000000">程序清单11:
- <script language="javascript">
- function leak(flag){
- var hostElement = document.getElementById("myDiv");
- for(var i = 0; i < 5000; i++){
- var parentDiv = document.createElement("<div onclick='foo()'>");
- var childDiv = document.createElement("<div onclick='foo()'>");
- parentDiv.appendChild(childDiv); //先加入节点parentDiv子节点childDiv
- hostElement.appendChild(parentDiv); //再将节点parentDiv加入父节点hostElement
- parentDiv = null;
- childDiv = null;
- hostElement.innerHTML = "";
- }
- hostElement = null;
- }
- function foo(){
- }
- </script>
- <INPUT TYPE="button" VALUE="创建对象" ONCLICK="leak()" title="创建对象">
- <div id="myDiv"></div>
- </font></font>
上述代码会出现内存暴涨,仔细拆解代码,发现需要同时具备以下条件才会出现:
1) IE浏览器
2) 先创建好子节点树后,再加入父节点
3) 事件方法使用文本,如: document.createElement("<div onclick='foo()'>");
发现问题了,解决就简单多了。
方法1:改变节点添加顺序
- <font size="3"><font color="#000000">parentDiv.appendChild(childDiv);
- hostElement.appendChild(parentDiv);
- 改为
- hostElement.appendChild(parentDiv);
- parentDiv.appendChild(childDiv);</font></font>
方法2:改变事件设置方法
- <font size="3"><font color="#000000">var parentDiv = document.createElement("<div onclick='foo()'>");
- var childDiv = document.createElement("<div onclick='foo()'>");
- 改为
- var parentDiv = document.createElement("div");
- parentDiv.onclick = foo;
- var childDiv = document.createElement("div");
- childDiv.onclick = foo;</font></font>
浏览器内部引用造成的内存异常,我想在未知的条件下,需要多测试,多去发现。
以上代码都在IE7上进行过测试,Firefox一分钟左右回收一次,而且内存增加也不多,回收也不多,不能做出结论。先到这吧,若有不对,还望大家不吝批评。