常规循环引用内存泄漏和Closure内存泄漏

来源:互联网 发布:数据分析法的书籍 编辑:程序博客网 时间:2024/05/16 05:00
导读:

  要了解javascript的内存泄漏问题,首先要了解的就是javascript的GC原理。

  我记得原来在犀牛书《JavaScript: The Definitive Guide》中看到过,IE使用的GC算法是计数器,因此只碰到循环 引用就会造成memory leakage。后来一直觉得和观察到的现象很不一致,直到看到Eric的文章,才明白犀牛书的说法没有说得很明确,估计该书成文后IE升级过算法吧。在IE 6中,对于javascript object内部,jscript使用的是mark-and-sweep算法,而对于javascript object与外部object(包括native object和vbscript object等等)的引用时,IE 6使用的才是计数器的算法。

  Eric Lippert在一文中提到IE 6中JScript的GC算法使用的是nongeneration mark-and-sweep。对于javascript对算法的实现缺陷,文章如是说:

  "The benefits of this approach are numerous, but the principle benefit is that circular references are not leaked unless the circular reference involves an object not owned by JScript. "

  也就是说,IE 6对于纯粹的Script Objects间的Circular References是可以正确处理的,可惜它处理不了的是JScript与Native Object(例如Dom、ActiveX Object)之间的Circular References。

  所以,当我们出现Native对象(例如Dom、ActiveX Object)与Javascript对象间的循环引用时,内存泄露的问题就出现了。当然,这个bug在IE 7中已经被修复了[]。中有个示意图和简单的例子体现了这个问题:

  ????????????????????
????

  

  上面这个例子,看似很简单就能够解决内存泄露的问题。可惜的是,当我们的代码中的结构复杂了以后,造成循环引用的原因开始变得多样,我们就没法那么容易观察到了,这时候,我们必须对代码进行仔细的检查。

  尤其是当碰到Closure,当我们往Native对象(例如Dom对象、ActiveX Object)上绑定事件响应代码时,一个不小心,我们就会制造出Closure Memory Leak。其关键原因,其实和前者是一样的,也是一个跨javascript object和native object的循环引用。只是代码更为隐蔽,这个隐蔽性,是由于javascript的语言特性造成的。但在使用类似内嵌函数的时候,内嵌的函数有拥有一个reference指向外部函数的scope,包括外部函数的参数,因此也就很容易造成一个很隐蔽的循环引用,例如:

  DOM_Node.onevent ->function_object.[ [ scope ] ] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。

  ????????????????????
????

  

  还有这个例子在IE 6中同样原因会引起泄露

  function?leakmaybe()?{var?elm?=?document.createElement("DIV");

  elm.onclick?=?function()?{return?2?+?2??}

  }for?(var?i?=?0?i??10000?i++)?{

  leakmaybe();

  }

  btw:

  关于Closure的知识,大家可以看看这篇文章,习惯中文也可以看看zkjbeyond的blog,他对Closure这篇文章进行了简要的翻译:。之所以会有这一系列的问题,关键就在于javascript是种函数式脚本解析语言,因此javascript中“函数中的变量的作用域是定义作用域,而不是动态作用域”,这点在犀牛书《JavaScript: The Definitive Guide》中的“Funtion”一章中有所讨论。中也对这个问题举了很详细的例子。

  一些 简单的解决方案

  目前大多数ajax前端的javascript framework都利用对事件的管理,解决了该问题。

  如果你需要自己解决这个问题,可以参考以下的一些方法:

  http://outofhanwell.com/ieleak/index.php?title=Main_Page:有个不错的检测工具中提到:可以利用递归Dom树,解除event绑定,从而解除循环引用:

  

  if (window.attachEvent) {

  var clearElementProps = [

  'data',

  'onmouseover',

  'onmouseout',

  'onmousedown',

  'onmouseup',

  'ondblclick',

  'onclick',

  'onselectstart',

  'oncontextmenu'

  ];

  window.attachEvent("onunload", function() {

  var el;

  for(var d = document.all.length;d--;){

  el = document.all[d];

  for(var c = clearElementProps.length;c--;){

  el[clearElementProps[c]] = null;

  }

  }

  });

  }

  

  

  而一文中则通过增加EventCache,从而给出一个相对结构化的解决方案

  /*????EventCache?Version?1.0

  Copyright?2005?Mark?Wubben

  Provides?a?way?for?automagically?removing?events?from?nodes?and?thus?preventing?memory?leakage.

  See??for?more?information.

  

  This?software?is?licensed?under?the?CC-GNU?LGPL?*/

  /*????Implement?array.push?for?browsers?which?don't?support?it?natively.

  Please?remove?this?if?it's?already?in?other?code?*/

  if(Array.prototype.push?==?null){

  Array.prototype.push?=?function(){

  for(var?i?=?0?i?  this[this.length]?=?arguments[i];

  };

  return?this.length;

  };

  };/*????Event?Cache?uses?an?anonymous?function?to?create?a?hidden?scope?chain.

  This?is?to?prevent?scoping?issues.?*/

  var?EventCache?=?function(){

  var?listEvents?=?[];

  

  return?{

  listEvents?:?listEvents,

  

  add?:?function(node,?sEventName,?fHandler,?bCapture){

  listEvents.push(arguments);

  },

  

  flush?:?function(){

  var?i,?item;

  for(i?=?listEvents.length?-?1?i?>=?0?i?=?i?-?1){

  item?=?listEvents[i];

  

  if(item[0].removeEventListener){

  item[0].removeEventListener(item[1],?item[2],?item[3]);

  };

  

  /*?From?this?point?on?we?need?the?event?names?to?be?prefixed?with?'on"?*/????????????????if(item[1].substring(0,?2)?!=?"on"){

  item[1]?=?"on"?+?item[1];

  };

  

  if(item[0].detachEvent){

  item[0].detachEvent(item[1],?item[2]);

  };

  

  item[0][item[1]]?=?null????????????};

  }

  };

  }();

  使用方法也很简单:

  

  

  

  

  /*?*?EventManager.js

  *?by?Keith?Gaughan

  *

  *?This?allows?event?handlers?to?be?registered?unobtrusively,?and?cleans

  *?them?up?on?unload?to?prevent?memory?leaks.

  *

  *?Copyright?(c)?Keith?Gaughan,?2005.

  *

  *?All?rights?reserved.?This?program?and?the?accompanying?materials

  *?are?made?available?under?the?terms?of?the?Common?Public?License?v1.0

  *?(CPL)?which?accompanies?this?distribution,?and?is?available?at

  *?http://www.opensource.org/licenses/cpl.php

  *

  *?This?software?is?covered?by?a?modified?version?of?the?Common?Public?License

  *?(CPL),?where?Keith?Gaughan?is?the?Agreement?Steward,?and?the?licensing

  *?agreement?is?covered?by?the?laws?of?the?Republic?of?Ireland.

  */

  //?For?implementations?that?don't?include?the?push()?methods?for?arrays.

  if?(!Array.prototype.push)?{

  Array.prototype.push?=?function(elem)?{

  this[this.length]?=?elem;

  }

  }var?EventManager?=?{

  _registry:?null,

  Initialise:?function()?{

  if?(this._registry?==?null)?{

  this._registry?=?[];

  //?Register?the?cleanup?handler?on?page?unload.

  EventManager.Add(window,?"unload",?this.CleanUp);

  }

  },

  /**

  *?Registers?an?event?and?handler?with?the?manager.

  *

  *?@param??obj?????????Object?handler?will?be?attached?to.

  *?@param??type????????Name?of?event?handler?responds?to.

  *?@param??fn??????????Handler?function.

  *?@param??useCapture??Use?event?capture.?False?by?default.

  *?????????????????????If?you?don't?understand?this,?ignore?it.

  *

  *?@return?True?if?handler?registered,?else?false.

  */????Add:?function(obj,?type,?fn,?useCapture)?{

  this.Initialise();

  //?If?a?string?was?passed?in,?it's?an?id.

  if?(typeof?obj?==?"string")?{

  obj?=?document.getElementById(obj);

  }

  if?(obj?==?null?||?fn?==?null)?{

  return?false????????}

  //?Mozilla/W3C?listeners?

  if?(obj.addEventListener)?{

  obj.addEventListener(type,?fn,?useCapture);

  this._registry.push({obj:?obj,?type:?type,?fn:?fn,?useCapture:?useCapture});

  return?true????????}

  //?IE-style?listeners?

  if?(obj.attachEvent?&&?obj.attachEvent("on"?+?type,?fn))?{

  this._registry.push({obj:?obj,?type:?type,?fn:?fn,?useCapture:?false});

  return?true????????}

  return?false????},

  /**

  *?Cleans?up?all?the?registered?event?handlers.

  */????CleanUp:?function()?{

  for?(var?i?=?0?i?  with?(EventManager._registry[i])?{

  //?Mozilla/W3C?listeners?

  if?(obj.removeEventListener)?{

  obj.removeEventListener(type,?fn,?useCapture);

  }

  //?IE-style?listeners?

  else?if?(obj.detachEvent)?{

  obj.detachEvent("on"?+?type,?fn);

  }

  }

  }

  //?Kill?off?the?registry?itself?to?get?rid?of?the?last?remaining

  //?references.

  EventManager._registry?=?null????}

  };

  使用起来也很简单

  

  

  >
  

  

  

  function onLoad() {

  EventManager.Add(document.getElementById(testCase),click,hit );

  returntrue;

  }

  function hit(evt) {

  alert(click);

  }

  

  

  

  


  

Click me!



  


  

  

  

  

  google map api同样提供了一个类似的函数用在页面的unload事件中,解决Closure带来的内存泄露问题。

  当然,如果你不嫌麻烦,你也可以为每个和native object有关的就阿vascript object编写一个destoryMemory函数,用来手动调用,从而手动解除Dom对象的事件绑定。

  还有一种就是不要那么OO,抛弃Dom的一些特性,用innerHTML代替appendChild,避开循环引用。详细见中的讨论贴。

  Cross-Page Leaks

   Cross-Page Leaks和下一节提到的Pseudo-Leaks在我看来,就是IE的bug, 虽然MS死皮赖脸不承认:)

  大家可以看看这段例子代码:

  ????????????????????Memory?Leaking?Insert????????Clean?Insert????????
????

  

  LeakMemory和CleanMemory这两段函数的唯一区别就在于他们的代码的循序,从代码上看,两段代码的逻辑都没有错。

  但LeakMemory却会造成泄露。原因是LeakMemory()会先建立起parentDiv和childDiv之间的连接,这时候,为了让 childDiv能够获知parentDiv的信息,因此IE需要先建立一个临时的scope对象。而后parentDiv建立了和 hostElement对象的联系,parentDiv和childDiv直接使用页面document的scope。可惜的是,IE不会释放刚才那个临时的scope对象的内存空间,直到我们跳转页面,这块空间才能被释放。而CleanMemory函数不同,他先把parentDiv和 hostElement建立联系,而后再把childDiv和parentDiv建立联系,这个过程不需要单独建立临时的scope,只要直接使用页面 document的scope就可以了, 所以也就不会造成内存泄露了

  btw:

  IE 6中垃圾回收算法,就是从那些直接"in scope"的对象开始进行mark清除的:

  Every variable which is "in scope" is called a "scavenger". A scavenger may refer to a number, an object, a string, whatever. We maintain a list of scavengers – variables are moved on to the scav list when they come into scope and off the scav list when they go out of scope.

  Pseudo-Leaks

  这个被称为“秀逗泄露”真是恰当啊:)

  看看这个例子:

  ????????????????function?LeakMemory()

  {

  //?Do?it?a?lot,?look?at?Task?Manager?for?memory?response

  for(i?=?0?i?  {

  hostElement.text?=?"function?foo()?{?}"//看内存会不断增加

  }

  }

  ");>
  

  MS是这么解释的,这不是内存泄漏。如果您创建了许多无法获得也无法释放的对象,那才是内存泄漏。在这里,您将创建许多元素,Internet Explorer 需要保存它们以正确呈现页面。Internet Explorer并不知道您以后不会运行操纵您刚刚创建的所有这些对象的脚本。当页面消失时(当您浏览完,离开浏览器时)会释放内存。它不会泄漏。当销毁页面时,会中断循环引用。

  唉~~~

  其它一些琐碎的注意点

  变量定义一定要用var,否则隐式声明出来的变量都是全局变量,不是局部变量;

  全局变量没用时记得要置null;

  注意正确使用delete,删除没用的一些函数属性;

  注意正确使用try...cache,确保去处无效引用的代码能被正确执行;

  open出来的窗口即使close了,它的window对象还是存在的,要记得删除引用;

  frame和iframe的情况和窗口的情况类似。

  参考资料(这是DHTML Leaks Like a Sieve)一文在google上的cache,原文已经连不上了)



本文转自

http://www.blogjava.net/tim-wu/archive/2006/05/29/48729.html
");>>
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 佳合家美客户使用案例 佳合家美产品价格多少 零美云合 纂美合 合美 不合群 合群 不合群的人 不合群怎么办 5岁孩子不合群怎么办 孩子不合群的原因 幼儿不合群的解决方法 不合群的原因 四岁孩子不合群 孩子幼儿园不合群 孩子在学校不合群 小朋友在幼儿园不合群 成功改掉儿子上幼儿园不合群 合羽 聚胜万合 合肥蜀山 合肥工业 合肥肥西 合肥学院 合肥地铁 合肥肥东 合肥巢湖 合肥长丰 合肥庐江 合肥4号线 合肥邮编 合肥师范 合肥高铁 合肥省 合肥好玩 安徽合肥 合肥特产 合肥哪个省 阜阳合肥 合肥南站 蚌埠合肥