陈惠超的博客Github

JavaScript 垃圾回收算法

2019-10-20#技术#JavaScript

像 C 语言这样的底层语言一般都有底层的内存管理接口。但是相反,JavaScript 并不提供这样的接口。JavaScript 是在创建变量时自动分配内存,并且在不使用它们时自动释放。释放的过程称为垃圾回收。所以说 JavaScript 具有自动垃圾收集的机制。原理很简单:就是找出那些不再使用的变量,然后再释放内存。
不管什么语言,内存的生命周期都一样:
1.
分配你所需要的内存
2.
使用分配到的内存
3.
不需要时将其释放。
垃圾回收算法
引用计数算法
引用的概念:在内存管理的环境中,一个对象如果有访问另外一个对象的权限,叫做一个对象引用另外一个对象。
这是最初级的垃圾收集算法。引用计数将“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(也就是零引用),对象将被垃圾回收机制回收。
工作原理
该算法的含义是跟踪记录每个值被引用的次数。当声明零一个变量并将一个变量引用类型赋值给该变量时,则这个值的引用次数是 1.如果同一个值又被赋给另外一个值,则改值的引用次数加 1。相反,如果包含对这个值引用的变量又取得另外一个值,则这个值的引用次数减 1.当这个值的引用次数变成 0 时,则说明没有办法再访问该值了,因而就可以将其占用的空间回收回来。这样当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
// 创建两个对象, 一个作为最外层对象属性的引用,一个被分配给变量 o var o = { a: { b: 2, }, } var o2 = o // o2 是第二个对这个对象的引用 o = 1 // o 赋值为1,解除对原始对象的引用 var oa = o2.a // 引用原始对象 a 的属性。现在这个对象有两个引用:o2,和 oa o2 = "yo" // o2 重新赋值,解除引用。所以原始的对象现在是零引用。 oa = null // oa 重新赋值,解除 a 属性的引用,a属性对应的那个对象也是处于零引用
JavaScript
限制
该算法有个限制:无法处理循环引用的例子。
var a = {} var b = {} a.ref = b b.ref = a
JavaScript
根据引用计数的原理,在这种情况下,这两个对象的内存永远都无法释放。
标记-清除算法 mark-and-sweep
标记清除法是 JavaScript 常用的垃圾收集方式。它将“对象是否不再需要”简化定义为“对象是否可以获得”。 该算法比引用计数算法好,所有现代浏览器都使用了该算法,并基于该算法进行改进。
工作原理
这个算法假定设置一个叫 root(在 JavaScript 里,root 就是全局对象)的对象。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找到这些对象引用的对象,再依次寻找引用对象的引用对象…。这样就能从 root 对象开始,垃圾回收器将可以找到所有可以找到的对象和收集所有不能获得的对象。 那些从根部出发不能获得的对象被标记为不再使用,稍后进行回收。
所以可以看出,一个零引用的对象总是不可获得。但是反之不能成立。参考上面循环引用的例子,a、b 执行完后属于不可获得对象,但是却拥有着相互的引用。 所以标记清除算法的循环引用在这里并不是一个问题。
限制
该算法的限制在于:对于那些无法从根对象查询到的对象都将被清除。尽管这是一个限制,但是实践中我们却很少会碰到类似的情况。
V8 引擎的垃圾回收算法
由于不同的对象生命周期不同,所以无法只采用一种回收策略来解决问题。所以 V8 采用的是分代式垃圾回收机制。因此 V8 将内存分为新生代和老生代两部分。 新生代中的对象存活时间较短,老生代中的对象存活时间较长或常驻内存的对象。分别对新老生代采用不同的垃圾回收算法来提高效率。
对象一开始都是分配到新生代,在满足某些条件后会被移动到老生代,这个过程也叫做晋升
新生代算法
新生代使用 Scavenge GC 算法。
工作原理
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是处于使用状态(From),另一个空间是空闲状态(To)。
新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会被销毁。当复制完成后将 From 空间和 To 空间互换,这样就完成了一次垃圾回收。
优缺点
优点:Scavenge 算法由于只复制存活的对象,而且通常大部分新生代空间的对象存活时间都不长,因此它在时间效率上有良好的表现。
缺点:但因为 Scavenge 操作需要 From、To 两个空间,因此它只适合小内存,一旦超过数 MB 的内存,Scavenge 就不适合了。
晋升
当一个对象经过多次复制后依然存活的话,它就会被认为是生命周期较长的对象。这种生命周期较长的对象会被移动到老生代中,采用新的算法进行管理。
对象从新生代移动到老生代的过程叫做晋升
对象晋升有两个条件:
一个对象是第二次经历从 From 空间复制到 To 空间,那么这个对象就会被移动到老生代中
当要从 From 空间复制一个对象到 To 空间时,如果 To 空间已经使用了超过 25%,那么这个对象会直接被移动到老生代。
老生代算法
老生代中的对象一般存活时间较长且数量也多或常驻内存的对象,如果还使用 Scavenge 的话,会存在两个问题:
老生代的对象存活时间较长且数量多,复制存活对象效率低;
老生代对象内存占用较大,使用 Scavenge 会浪费一半的内存
所以老生代采用了标记清除算法(mark-sweep)和标记压缩算法(mark-compact)。
标记清除
标记清除分为标记和清除两个阶段。 在标记阶段,会遍历堆内存中的所有对象,并标记存活的对象,在随后的清除阶段,只清除没有被标记的对象,即失活的对象。
优缺点
缺点:标记清除最大的问题在于当进行了一次垃圾回收后,内存空间会出现不连续的状态。这种内存碎片会对后续对内存分配造成问题。
所有有了标记压缩。
标记压缩
标记压缩就是为了解决标记清除所带来对内存碎片问题。它在标记清除的基础上演变而来。 mark-compact 在标记完存活对象以后,会将存活对象向内存空间的一端移动,移动完成后,直接清理边界外的所有内存。
限制
由于 mark-compact 需要移动对象,因此执行速度不可能很快,非常耗时。
老生代算法的回收策略中,是 mark-sweep 和 mark-compact 相结合的方式。由于 mark-compact 耗时的原因,主要以 mark-sweep 为主。在空不足以对新生代中晋升过来的对象进行分配时,才使用 mark-compact。
参考资料
JavaScript 高级程序设计