JS垃圾回收机制

序言

一般的浏览器都具有Javascript垃圾回收机制(GC:Garbage Collection),也就是说执行环境会负责管理代码执行过程中使用的内存,这个过程是不可见的,我们创建的基本类型,函数,对象,数组等等,都需要内存,同时也都需要回收

当不再需要某样东西时,javascript引擎就会发现并清理它,具体是怎么实现的呢?

可达性

JS管理内存有一个主要概念就是可达性。
简单来说,可达性就是可以以某种方法访问或引用的值,他们被保证存储在内存中。

根——固定的可达值,永远不会被回收

  • 本地函数的局部变量与参数
  • 当前调用函数的作用域链上的变量与参数
  • 全局变量

如果引用或引用链可以从根访问到其他任何值,则认为该值是可以访问的

例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的

一个例子

1
2
3
let user = {
name:'tom'
}


需要注意的是,这里需要转变一下观念,代码里的user和 {name:“John”},实际上是两个对象,而这里的箭头,指的就是user引用了对象 {name:“John”}

这个时候如果user的值被覆盖,引用丢失:

1
user = null


那么很显然, {name:“John”}将没有任何办法能够引用和访问到它,垃圾回收将丢弃这个对象并释放内存

两个引用

而对于下面这个例子

1
2
3
4
5
let user = {
name:'tom'
}
let admin = user
user = null


最终该对象是可以通过admin全局变量访问的,所以即使user被覆盖,也依然可以通过admin访问,对象可达,所以不会被回收

相互关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function marry (man, woman) {
woman.husban = man;
man.wife = woman;

return {
father: man,
mother: woman
}
}

let family = marry({
name: "John"
}, {
name: "Ann"
})

函数 marry 通过给两个对象彼此提供引用来“联姻”它们,并返回一个包含两个对象的新对象,这个时候他们的内存结构是这样的:

到目前为止所有对象都是可以访问的

这个时候删除两个引用:

1
2
family.father = null
family.mother.husband = null

这个时候我们发现,已经没有任何方法途径可以访问和引用左下角的这个对象了:


垃圾回收后:

无法访问的数据块

有可能整个相互连接的对象变得不可访问并从内存中删除。
例如上面的例子:

1
family = null

这个时候内存的结构变成了:

由于family对象已经从根上断开了连接,所以marry函数内部的变量,参数都会被删除

垃圾回收算法

一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。

标记清除

这是一个最常用的回收算法,定期执行以下垃圾回收步骤:

  • 垃圾回收器获取“标记”他们
  • 然后访问并“标记”所有他们的引用
  • 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
  • 以此类推,直到有未访问的引用(可以从根访问)为止。
  • 除标记的对象外,所有对象都被删除。

举个例子,现有一个对象结构如下:

第一步:标记根

第二步:标记他们的引用

第三步:以此类推,标记子孙代的引用:

第四步:没有被标记的对象被清除

引用计数

引用计数的含义是跟踪记录每个值被引用的次数。

当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。

这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。

1
2
3
4
5
6
function test() {
var a = {}; // {}的引用次数为1
var b = a; // {}的引用次数加1,为2
var c = a; // {}的引用次数再加1,为3
var b = {}; // {}的引用次数减1,为2
}

其实读者在看完上面这段描述,再结合标记清除,很快就会发现,引用计数对于那种相互引用产生的数据块会产生严重的问题:他们的引用数量永远不会是0

1
2
3
4
5
6
function fn() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}

正如上面这个例子所说,他们之间的内存结构如下:


当函数执行完毕或者说直接就没有执行时:a与b的引用次数都不为0,但是他们整体的代码块是不可达的,所以可以使用标记清除来回收他们的内存,可是引用计数就束手无策了。

如果使用引用计数,当fn函数被大量调用,可以想象,其内存占用将直线上升

虽然在如今的浏览器中基本都是使用标记清除,但是!!!IE这个奇葩又来了……..

IE 中有一部分对象并不是原生 JS 对象。例如,其内存泄露 DOM 和 BOM 中的对象就是使用 C++ 以 COM 对象的形式实现的,而 COM 对象的垃圾回收机制采用的就是引用计数策略。因此,即使IE的js引擎采用标记清除策略来实现,但 JS 访问的COM对象依然是基于引用计数策略的换句话说,只要在 IE 中涉及 COM 对象,就会存在循环引用的问题

1
2
3
4
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.e = element;
element.o = myObject;

这个例子在一个 DOM 元素 element 与一个原生js对象 myObject 之间创建了循环引用。其中,变量 myObject 有一个属性 e 指向 element 对象;而变量 element 也有一个属性 o 回指 myObject。由于存在这个循环引用,即使例子中的 DOM 从页面中移除,它也永远不会被回收。

两个实际性的例子:

第一个:

  • 黄色是指直接被 js变量所引用,在内存里
  • 红色是指由于DOM树的连接关系,间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的
  • 子元素 refB 由于 parentNode指针 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除
  • 换句话说:要将上面有颜色的块删除,必须同时删除refA和refB,否则都不行

第二个(这个例子简直——绝了,我都惊呆了)

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

这段代码看起来没什么问题,但是 obj 引用了 document.getElementById(‘element’),而 document.getElementById(‘element’) 的 onclick 方法会引用外部环境中的变量(outerFunction),自然也包括 obj,obj又引用了document.getElementById(‘element’) 。
是不是很隐蔽啊。(在比较新的浏览器中在移除Node的时候已经会移除其上的event了,但是在老的浏览器,特别是 IE 上会有这个 bug)

解决办法:

最简单的方式就是自己手工解除循环引用,比如刚才的函数可以这样

1
2
3
4
5
window.onload=function outerFunction(){
var obj = document.getElementById("element");
obj.onclick=function innerFunction(){};
obj=null;
};

终于:IE9+ 并不存在循环引用导致 DOM 内存泄露问题,可能是微软做了优化,或者 DOM 的回收方式已经改变。

垃圾回收的触发时机:

垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量、4096个对象、64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。

微软在 IE7 中做了调整,触发条件不再是固定的,而是动态修改的,初始值和 IE6 相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于 85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作智能了很多

GC优化策略

分代回收

这个和Java回收策略思想是一致的,也是V8所主要采用的。目的是通过区分“临时”与“持久”对象;多回收“临时对象”区(young generation),少回收“持久对象”区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。如图:
一些优化:

增量回收

这个方案的思想很简单,就是“每次处理一点,下次再处理一点,如此类推”。如图:

空闲时间收集

垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。

内存溢出带来的影响

JS 程序的内存溢出后,会使某一段函数体永远失效(取决于当时的 JS 代码运行到哪一个函数),通常表现为程序突然卡死或程序出现异常。

可能的泄露点

  • DOM/BOM 对象泄漏;
  • script 中存在对 DOM/BOM 对象的引用导致;
  • JS 对象泄漏;
  • 通常由闭包导致
  • 事件处理回调,导致 DOM 对象和脚本中对象双向引用,这个是常见的泄漏原因;

代码关注点

  • DOM 中的 addEventLisner 函数及派生的事件监听
  • 其它 BOM 对象的事件监听, 比如 websocket 实例的 on 函数;
  • 避免不必要的函数引用;
  • 如果使用 render 函数,避免在 HTML 标签中绑定 DOM/BOM 事件;
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2015-2021 AURORA_ZXH
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信