js晋级篇——前端内存泄漏探讨

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了js晋级篇——前端内存泄漏探讨相关的知识,希望对你有一定的参考价值。

1.IE7/8 DOM对象或者ActiveX对象循环引用导致内存泄漏


  循环引用分为两种:

  第一种:多个对象循环引用

var a=new Object;
var b=new Object;
a.r=b;
b.r=a;

  第二种:循环引用自己

var a=new Object;
a.r=a;

  对于ECMAScript 对象而言,只要没有其他对象引用对象 a、b,也就是说它们只是相互之间的引用,那么仍然会被垃圾收集系统识别并处理。

  但是,在 IE7、IE8 中,如果循环引用中的任何对象是 DOM 节点或者 ActiveX 对象,比如var a = document.getElementById("#a"),垃圾收集系统则不会发现它们之间的循环关系,因为IE的DOM回收机制和JS回收机制不是同一个。js回收机制分两种:标记清除引用计数引用计数对循环引用的垃圾回收会出现内存泄漏,而IE的DOM回收机制便是采用引用计数的。IE9+并不存在循环引用导致Dom内存泄露问题,可能是微软做了优化,或者Dom的回收方式已经改变。

下面摘自小苹果的跟我学习javascript的垃圾回收机制与内存管理

二、标记清除
js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
function test(){
 var a = 10 ; //被标记 ,进入环境 
 var b = 20 ; //被标记 ,进入环境
}
test(); //执行完毕 之后 a、b又被标离开环境,被回收。
  垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
  到目前为止,IE、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
三、引用计数   引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
function test(){ var a = {} ; //a的引用次数为0 var b = a ; //a的引用次数加1,为1 var c =a; //a的引用次数再加1,为2 var b ={}; //a的引用次数减1,为1 }   Netscape Navigator3是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。 function fn() { var a = {}; var b = {}; a.pro = b; b.pro = a; } fn();   以上代码a和b的引用次数都是2,fn()执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为a和b的引用次数不为0,所以不会被垃圾回收器回收内存,如果fn函数被大量调用,就会造成内存泄露。在IE7与IE8上,内存直线上升。   我们知道,IE中有一部分对象并不是原生js对象。例如,其内存泄露DOM和BOM中的对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数策略。因此,即使IE的js引擎采用标记清除策略来实现,但js访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题。 var element = document.getElementById("some_element"); var myObject = new Object(); myObject.e = element; element.o = myObject;   这个例子在一个DOM元素(element)与一个原生js对象(myObject)之间创建了循环引用。其中,变量myObject有一个名为element的属性指向element对象;而变量element也有一个属性名为o回指myObject。由于存在这个循环引用,即使例子中的DOM从页面中移除,它也永远不会被回收。   看上面的例子,有同学回觉得太弱了,谁会做这样无聊的事情,其实我们是不是就在做 window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction(){}; };   这段代码看起来没什么问题,但是obj引用了document.getElementById(“element”),而document.getElementById(“element”)的onclick方法会引用外部环境中的变量,自然也包括obj,是不是很隐蔽啊。   解决办法   最简单的方式就是自己手工解除循环引用,比如刚才的函数可以这样 myObject.element = null; element.o = null; window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction(){}; obj=null; };   将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。   要注意的是,IE9+并不存在循环引用导致Dom内存泄露问题,可能是微软做了优化,或者Dom的回收方式已经改变

  在上面描述的循环引用例子

window.onload=function outerFunction(){
var obj = document.getElementById("element");
  obj.onclick=function innerFunction(){};
};

   还可以理解成闭包循环引用导致的内存泄漏。怎么理解?

  首先obj是外部的一个对象, obj.onclick定义的这个函数隐式的调用到了obj这个对象(obj.onclick函数中的this就是对象obj)。然后我们需要知道obj.onclick实际上是一个outerFunction外部的函数,为什么?DOM监听事件不可能是局部作用域的,是全局作用域的,明白了吧。所以DOM触发这个事件相当于是在函数outerFunction外部调用了obj.click(),而事件内部使用了outerFunction的变量obj,这就形成了一个闭包。IE7/IE8 DOM的引用计数永远无法回收这个DOM对象。无论如何,这都是DOM循环引用导致的内存泄漏,普通闭包是不会导致内存泄漏的。

  改成如下结构

window.onload=function outerFunction(){
  var obj = document.getElementById("element");
  $(obj).click(function innerFunction(){});
};

   jQuery绑定事件最终都没有直接绑定到DOM对象上,而是使用jQuery缓存来绑定的。详见jQuery事件体系结构

  即使此时仍然会创建一个闭包,并且也会导致同前面一样的循环,但这里的代码却不会使 IE 发生内存泄漏。由于jQuery考虑到了内存泄漏的潜在危害,所以它会手动释放自己指定的所有事件处理程序(jQuery源代码$.fn.remove函数中有对节点的缓存释放的处理)。只要坚持使用jQuery的事件绑定方法,就无需为这种特定的常见原因导致的内存泄漏而担心。

  但是,这并不意味着我们完全脱离了险境。当对DOM元素进行其他操作时,仍然要处处留心。只要是将JavaScript对象指定给DOM元素,就可能在旧版本IE中导致内存泄漏。jQuery只是有助于减少发生这种情况的可能性。

  有鉴于此,jQuery为我们提供了另一个避免这种泄漏的工具。用.data()方法,将信息附加到DOM元素。由于这里的数据并非直接保存在扩展属性中(jQuery使用一个内部对象并通过它创建的ID来保存这里所说的数据),因此永远也不会构成引用循环,从而有效回避了内存泄漏问题。这种方式也就是jQuery事件绑定使用的方式。

  

2.基础的DOM泄漏


当原有的DOM被移除时,子结点引用没有被移除则无法回收

var select = document.querySelector;
var treeRef = select(‘#tree‘);

var leafRef = select(‘#leaf‘);   //在COM树中leafRef是treeFre的一个子结点

select(‘body‘).removeChild(treeRef);//#tree不能被回收入,因为treeRef还在

   解决方法:

treeRef = null;//tree还不能被回收,因为叶子结果leafRef还在
leafRef = null;//现在#tree可以被释放了

  

DOM 插入顺序导致内存泄漏

  当 动态创建的2 个不同范围的 DOM 对象附加到一起的时候,一个临时的对象会被创建。这个 DOM 对象改变范围到 document 时,那个临时对象就没用了,这个临时对象没有被回收将导致内存泄漏。如果我们一一将这两个DOM添加到原有的 本省存在的DOM 对象上就不会产生中间临时对象。详见理解和解决IE内存泄漏页面交叉泄露 Cross-Page Leaks。

 

3.timer定时器泄漏



var
val = 0; for (var i = 0; i < 90000; i++) { var buggyObject = { callAgain: function() { var ref = this; val = setTimeout(function() { ref.callAgain(); }, 90000); } } buggyObject.callAgain();

   这个时候你无法回收buggyObject

//虽然你想回收但是timer还在
buggyObject = null;

    解决办法,先停止timer然后再回收

//解决方法,先停止定时器
clearTimeout(val);
buggyObject = null;

  

推荐内存泄漏文章:

理解和解决IE内存泄漏(中文翻译):http://www.tuicool.com/articles/2AZ3y2

理解和解决IE内存泄漏(英文原版):https://msdn.microsoft.com/en-us/library/bb250448.aspx

js内存泄露的几种情况:http://blog.csdn.net/li2274221/article/details/25217297

 

  如果觉得本文不错,请点击右下方【推荐】!

以上是关于js晋级篇——前端内存泄漏探讨的主要内容,如果未能解决你的问题,请参考以下文章

垃圾回收 及 内存泄漏

清除 React Hooks 中未安装组件上的内存泄漏

闭包会造成内存泄漏吗?

闭包会造成内存泄漏吗?

识别并避免 Js 内存泄漏,跟低级缺陷say goodbye,让老总对你刮目相看

闭包会造成内存泄漏吗