winter-cn 浏览器中的内存泄露(重新整理ing)

引用自:winter-cn浏览器中的内存泄露(重新整理ing)感谢winter-cn整理出并分享这么好的文章。

什么是内存泄露

内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。在C++中,因为是手动管理内存,内存泄露是经常出现的事情。而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。

内存泄露Quick View

不同的浏览器中存在各种内存泄露方式,目前发现的主要是这样几种:

1. 循环引用

已经确认存在泄漏的浏览器:IE6.0 FF2.0

含有DOM对象的循环引用将导致大部分当前主流浏览器内存泄露 这里有两个简单的概念

引用:a.属性=b,a就引用了b

循环引用:简单来说假如a引用了b,b又引用了a,a和b就构成了循环引用。

a和b循环引用:

   1:  var a=new Object;
   2:  var b=new Object;
   3:  a.r=b;
   4:  b.r=a;

a循环引用自己:

   1:  var a=new Object;
   2:  a.r=a;

循环引用很常见且大部分情况下是无害的,但当参与循环引用的对象中有DOM对象或者ActiveX对象时,循环引用将导致内存泄露。我们把例子中的任何一个new Object替换成document.getElementById或者document.createElement就会发生内存泄露了。
尽管这看起来非常容易理解,但是因为有closure的参与而使事情变得复杂,有些closure导致的循环引用很难被察觉。下面是一个非常常见的动态绑定事件:

   1:  function bindEvent()
   2:  {
   3:      var obj=document.createElement("XXX");
   4:      obj.onclick=function(){
   5:          //Even if it's a empty function
   6:      }
   7:  }

这个bindEvent执行时100%会发生内存泄露,Someone 可能会问,哪里出现了循环引用? 关于closure和scope chain参与的循环引用比较复杂,此处暂不深入讨论。有一个简单的判断方式:函数将间接引用所有它能访问的对象。obj.onclick这个函数中 可以访问外部的变量obj 所以他引用了obj,而obj又引用了它,因此这个事件绑定将会造成内存泄露。在IBM的文章中介绍了2种方式解决类似的问题一个是obj=null,另一个是把onclick的函数写在bindEvent外,重复人家的我就不说了。简单贴下代码:

   1:  function bindEvent()
   2:  {
   3:      var obj=document.createElement("XXX");
   4:      obj.onclick=onclickHandler;
   5:  }
   6:  function onclickHandler(){
   7:      //do something
   8:  }
 
   1:  function bindEvent()
   2:  {
   3:      var obj=document.createElement("XXX");
   4:      obj.onclick=function(){
   5:          //Even if it's a empty function
   6:      }
   7:      obj=null;
   8:  }

这两个方法都打断了循环引用,可以解决问题,但是似乎对代码表达能力造成了一定破坏,假设有这么一个问题:

   1:  function bindEvent()
   2:  {
   3:      var obj=document.createElement("XXX");
   4:      var var0="OOXX";//Here is a variable不知道你们有没有注意到变量值。。。
   5:      obj.onclick=function(){
   6:          alert(var0);//I want to visit var2 here!
   7:      }
   8:      return obj;//bindEvent must return obj!
   9:  }

好了 这下两种办法都不行了,假如我把函数写外面去,var0肯定访问不了,假如我把obj弄成null,还怎么return它呢?这并不是空想的需要,这实际上是一个用JS定制DOM控件的简单抽象:创建DOM元素、设置私有属性、绑定事件。所以,我们必须update一下两个方法。首先,方法1,为了让函数能访问某些变量,我们可以通过一个Builder函数来订制onclick的外部闭包:

   1:  function bindEvent()
   2:  {
   3:      var obj=document.createElement("XXX");
   4:      var var0="OOXX";//Here is a variable
   5:      obj.onclick= onclickBuilder(var0);//想访问谁就把谁传进去!!
   6:      return obj;//bindEvent must return obj!

7: }//这里的解决办法很巧妙,将onclickBuilder写在函数外部,这样避免了事件动态的绑定匿名函数带来的内存泄露问题,但因为将该函数写在了外面,就无法

再通过闭包的形式访问bindEvent中的变量了,若想在eventListener中访问bindEvent中的变量,就需要使用传参,但传参的话无疑会执行onclickBuilder函数,本来只

是想为obj绑定click事件处理器,这里却直接执行了该事件处理器,明显不符逻辑,于是就在之后的onclickBuilder中将一个匿名函数返回,创建一个闭包,因为该闭

包在onclickBuilder中,所以可以访问传进来的参数var0。但有个疑问,既然通过函数定制返回了一个外部闭包,那不还是动态事件绑定么?那和最初的问题又有什么

区别?饶了一大圈,还是没解决根本问题?

   8:  function onclickBuilder(var0)//这里跟上面对应上就行了 最好参数名字也对应上
   9:  {
  10:      return function(){
  11:          alert(var0);
  12:      }
  13:  }

第二个办法,这个来自51js的chpn同学,让obj=null在return 之后执行!!

   1:  function bindEvent()
   2:  {
   3:      try{
   4:          var obj=document.createElement("XXX");
   5:          var var0="OOXX";//Here is a variable
   6:          obj.onclick=function(){
   7:              alert(var0);//I want to visit var2 here!
   8:          }
   9:          return obj;//bindEvent must return obj!
  10:      } finally {
  11:          obj=null;//这个就毋庸置疑了,null是必然会释放内存的
  12:      }
  13:  }
2. 某些DOM操作

这是IE系列的特有问题 简单的来说就是在向不在DOM树上的DOM元素appendChild,可能会发生内存泄露(只是可能,具体原因不明,似乎十分复杂,下面例子中去掉onClick也可以避免泄露)。所以appendChild的顺序可能影响内存泄露,来自微软的例子:

   1:  </html>
   2:      <head>
   3:          <script language="JScript">
   4:          function LeakMemory()
   5:          {
   6:              var hostElement = document.getElementById("hostElement");
   7:              // Do it a lot, look at Task Manager for memory response
   8:              for(i = 0; i < 5000; i++)
   9:              {
  10:                  var parentDiv =
  11:                      document.createElement("<div onClick='foo()'>");
  12:                  var childDiv =
  13:                      document.createElement("<div onClick='foo()'>");
  14:                  // This will leak a temporary object
  15:                  parentDiv.appendChild(childDiv);
  16:                  hostElement.appendChild(parentDiv);
  17:                  hostElement.removeChild(parentDiv);
  18:                  parentDiv.removeChild(childDiv);
  19:                  parentDiv = null;
  20:                  childDiv = null;
  21:              }
  22:              hostElement = null;
  23:          }
  24:   
  25:          function CleanMemory()
  26:          {
  27:              var hostElement = document.getElementById("hostElement");
  28:              // Do it a lot, look at Task Manager for memory response
  29:              for(i = 0; i < 5000; i++)
  30:              {
  31:                  var parentDiv =
  32:                      document.createElement("<div onClick='foo()'>");
  33:                  var childDiv =
  34:                      document.createElement("<div onClick='foo()'>");
  35:                  // Changing the order is important, this won't leak
  36:                  hostElement.appendChild(parentDiv);
  37:                 parentDiv.appendChild(childDiv);
  38:                  hostElement.removeChild(parentDiv);
  39:                  parentDiv.removeChild(childDiv);
  40:                  parentDiv = null;
  41:                  childDiv = null;
  42:              }
  43:              hostElement = null;
  44:          }
  45:          </script>
  46:      </head>
  47:      <body>
  48:          <button onclick="LeakMemory()">Memory Leaking Insert</button>
  49:          <button onclick="CleanMemory()">Clean Insert</button>
  50:          <div id="hostElement"></div>
  51:      </body>
  52:  </html>

而在IE7中,貌似为了改善内存泄露,IE7采用了极端的解决方案:离开页面时回收所有DOM树上的元素,其它一概不管。但是这不仅没起到任何作用,反而使问题变得更加复杂。对这类问题,除了自觉一点绕开这些恶心的东西,多用innerHTML这种无用的建议之外。我想可以通过覆盖document.createElement来略为改善:
首先我们定义一个看不见的元素当作垃圾箱,所有新创建的元素都扔进垃圾箱里,这样保证了所有DOM元素都在DOM树上,IE7就可以正确回收了,另一方面也能避免所谓的"appendChild顺序不对导致内存泄露"。

   1:  function MemoryFix(){
   2:      var garbageBox=document.createElement("div");
   3:      garbageBox.style.display="none";
   4:      document.body.appendChild(garbageBox);
   5:      var createElement=document.createElement;
   6:      document.createElement=function(){
   7:          var obj=Function.prototype.apply.apply(createElement,[document,arguments]);
   8:          garbageBox.appendChild(obj);
   9:          return obj;
  10:      }
  11:  }
3. 自动类型装箱转换

别不相信,下面的代码在ie系列中会导致内存泄露

   1:  var s=”lalala”;
   2:  alert(s.length);//看到这个,蛋疼的很

s本身是一个string而非object,它没有length属性,所以当访问length时,JS引擎会自动创建一个临时String对象封装s,而这个对象一定会泄露。
这个bug匪夷所思,所幸解决起来相当容易,记得所有值类型做.运算之前先显式转换一下:

   1:  var s="lalala";
   2:  alert(new String(s).length);
参考

Understanding and Solving Internet Explorer Leak Patterns(中文版)
Memory leak patterns in JavaScript(中文版)
51js的一则讨论

posted @ 2011-07-23 08:40  像阳光一样  阅读(396)  评论(0编辑  收藏  举报