categories: [前端,JavaScript]
thumbnail: /images/fe/gc.jpg
toc: true
序言
一般的浏览器都具有Javascript垃圾回收机制(GC:Garbage Collection),也就是说执行环境会负责管理代码执行过程中使用的内存,这个过程是不可见的,我们创建的基本类型,函数,对象,数组等等,都需要内存,同时也都需要回收
当不再需要某样东西时,javascript引擎就会发现并清理它,具体是怎么实现的呢?
可达性
JS管理内存有一个主要概念就是可达性。
简单来说,可达性就是可以以某种方法访问或引用的值,他们被保证存储在内存中。
根——固定的可达值,永远不会被回收
- 本地函数的局部变量与参数
- 当前调用函数的作用域链上的变量与参数
- 全局变量
如果引用或引用链可以从根访问到其他任何值,则认为该值是可以访问的
例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的
一个例子
let user = {
name:'tom'
}
需要注意的是,这里需要转变一下观念,代码里的user和 {name:“John”}
,实际上是两个对象,而这里的箭头,指的就是user引用了对象 {name:“John”}
。
这个时候如果user的值被覆盖,引用丢失:
user = null
那么很显然, {name:“John”}
将没有任何办法能够引用和访问到它,垃圾回收将丢弃这个对象并释放内存
两个引用
而对于下面这个例子
let user = {
name:'tom'
}
let admin = user
user = null
最终该对象是可以通过admin全局变量访问的,所以即使user被覆盖,也依然可以通过admin访问,对象可达,所以不会被回收
相互关联
function marry (man, woman) {
woman.husban = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
})
函数 marry 通过给两个对象彼此提供引用来“联姻”它们,并返回一个包含两个对象的新对象,这个时候他们的内存结构是这样的:
到目前为止所有对象都是可以访问的
这个时候删除两个引用:
family.father = null
family.mother.husband = null
这个时候我们发现,已经没有任何方法途径可以访问和引用左下角的这个对象了:
垃圾回收后:
无法访问的数据块
有可能整个相互连接的对象变得不可访问并从内存中删除。
例如上面的例子:
family = null
这个时候内存的结构变成了:
由于family对象已经从根上断开了连接,所以marry函数内部的变量,参数都会被删除
垃圾回收算法
一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。
标记清除
这是一个最常用的回收算法,定期执行以下垃圾回收步骤:
- 垃圾回收器获取根并**“标记”**他们
- 然后访问并“标记”所有他们的引用
- 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
- 以此类推,直到有未访问的引用(可以从根访问)为止。
- 除标记的对象外,所有对象都被删除。
举个例子,现有一个对象结构如下:
第一步:标记根
第二步:标记他们的引用
第三步:以此类推,标记子孙代的引用:
第四步:没有被标记的对象被清除
引用计数
引用计数的含义是跟踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。
function test() {
var a = {}; // {}的引用次数为1
var b = a; // {}的引用次数加1,为2
var c = a; // {}的引用次数再加1,为3
var b = {}; // {}的引用次数减1,为2
}
其实读者在看完上面这段描述,再结合标记清除,很快就会发现,引用计数对于那种相互引用产生的数据块会产生严重的问题:他们的引用数量永远不会是0
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 对象,就会存在循环引用的问题。
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,否则都不行
第二个(这个例子简直——绝了,我都惊呆了)
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)
解决办法:
最简单的方式就是自己手工解除循环引用,比如刚才的函数可以这样
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 事件;