“说一下JS的垃圾回收吧。”
“啊,垃圾,面试不上也不能骂人是垃圾吧...”
当然,这只是一个段子,但是,当面试官问你什么是垃圾回收时,你怎么说呢?
什么是垃圾
为了说明这一切,我来举一个美好的例子(本来有一个不美好版,被我废弃了 笑=-=)
520到了,一下班,就去花店排队半天,给女神买了一束玫瑰
这个时候这束玫瑰当然不是垃圾,因为需要它作为表白的筹码。
女神看到玫瑰,笑颜逐开地接了过去,害羞的笑了。
这个时候如果我说这是垃圾,你肯定得喷我...
过了许久,花儿谢了,你早上下楼时,女神说“xxx,那个花已经谢了,你出门的时候顺手扔到楼下垃圾桶一下吧。”
这个时候,这束玫瑰已经完成了他的使命,然后被扔到了垃圾桶里面。
所以根据这个故事,我们得出一个结论:
不再需要,即为垃圾
js垃圾回收机制
一般来说,编程语言中的垃圾回收分为两类:
- 手动回收
何时分配内存,何时销毁,都由代码控制 (如C/C++)
- 自动回收
产生的垃圾由垃圾回收器释放,而不需要手动控制(如Python/Java/JavaScript)
js中,采用的是后一种,即自动回收,也就是说,在实际开发中,我们不需要手动处理代码产生的垃圾,而是由程序自行处理。
垃圾标记规则
那么,浏览器怎么知道某个内存里存的内容是不是垃圾呢?
一般来说,对浏览器而言,它是不知道你不需要某个变量,但是知道你有可能需要它,所以所有的全局变量都不是垃圾。
有如下代码:
var a = {}
var b = a
a = null
b = null
var c = function() {
var d = {}
console.log(d)
}
c()
var e = function() {
var f = {a: 1}
return function h() {
return f.a;
}
}
var g = e()
g = null

{}被变量a和b所引用时,其不会被销毁,而当将引用断开,即将a和b都置为null,此时,{}就是在一个不会被用到的内存里,无法再与任何一个对象建立关系,下次垃圾回收会将其回收。
image-20200616222037016.png

第二部分中,函数作用域中的变量d在调用完成之后,就不会再被用到,下次再调用函数时,会创建一个新的变量d,所以函数每次执行完退出,其所创建的变量d也应该在下次垃圾回收时被回收掉。

第三部分中,e函数执行完成之后,其中的局部变量f被返回的匿名函数所需要,而该匿名函数则被变量g所引用,所以此时f不会被回收,这也就是日常开发中所用到的闭包,而在最后一步,g与一名函数之间的引用关系被断开,被置为null,此时匿名函数成为了垃圾,其所引用的局部变量f就成了垃圾。
function getTeamMember(frontEnd, backEnd) {
frondEnd.partner = backEnd; // 前端的搭档是后端
backEnd.partner = frontEnd; // 后端的搭档是前端
return {
frontEnd,
backEnd,
}
}
// 有一个团队,里面有一个前端,一个后端
var team = getTeamMember({name: 'jack', tech: 'react'}, {name: 'tracy': tech: 'golang'})
// 突然有一天,团队解散了(我还是写了一个悲伤的故事)
var team = null;
以上例子中,开始,一个团队中有两个技术人员,一个前端和一个后端,他们互为搭档,所以互相引用,同时,他们都是团队中的一员,被团队所引用。突然有一天,团队解散了,处理完赔偿相关事宜,team被置为null,也就是,这两人再也和团队没关系了,即使现在两人之间还互现存在引用,但是在当前环境中,已经没有任何变量在引用他们,所以在当前环境里他们也成了“垃圾”。

从上面的图可以看出,检查一个对象是否可能被需要,是考察它是否可以通过变量引用找到,也就是,看一个对象的可达性(reachability)。
规则:
- 全局变量都不是垃圾(你可能在任何时候,任意地方用到它)
- 局部变量,在函数退出后,成为了垃圾
- 没有任何变量引用该内存的地址,则内存所保存的内容即为垃圾
- 孤岛:一堆对象间相互引用,但是没有任何变量引用他们的内存地址(也称为环引用)
垃圾回收过程
要说垃圾回收,首先需要了解一个知识点,即所谓的代际假说
-
大部分对象在内存中存在的时间很短
-
一个对象已经存活了很久,它还会活很久
根据以上说法,在V8引擎中,堆(存放对象的地方)被分为了两个部分:
- 新生区:存放生存时间短的对象 由副垃圾回收器回收
- 老生区:存放生存时间长的对象,由主垃圾回收器回收
主垃圾回收器的回收采用标记扫除的方式进行垃圾回收(mark-sweep),主要分为三个步骤:
-
标记
从GC Root(全局window,DOM树等)触发,遍历所有对象
-
回收
回收非活动对象所占据内存
-
内存整理
回收对象后,内存中会出现大量不连续的空间
因为一次垃圾回收需要花费的时间很长,V8则采用比如增量回收,标记回收等方式进行优化。
而针对副垃圾回收器,则采用Scavenge算法进行回收,即将空间划分为两半,其中一半用来储存对象(from space),另一半则空闲(to space),完成标记后,将对象区还在使用的对象复制到空闲区,清除对象区所有内容,完成复制后,两个区域角色互换,如此往复,其中的步骤为“标记回收 —— 复制存活对象互换区域”,因为每次回收都会完整复制对象,所以不会产生不连续空间。

和主垃圾 回收器不同,副垃圾回收器需要保持该区域足够小,才能保证回收耗时足够短,所以对于一些大对象,在创建时就会直接被放入老生区,而对于多次回收之后还存在的对象,根据代际假说的第二点,采用对象晋升策略,将其转移到老生区进行存储。