W

浏览器垃圾回收

2024-06-29 · 13min

什么是垃圾回收

js 中垃圾回收(Garbage Collection GC)一种自动管理内存的机制,用于检测和清除不再活跃的对象,从而释放内存空间

目的:减少内存泄漏,提高程序运行效率

垃圾回收算法

引用计数法

通过对象被引用的次数来判断对象是否存活。

const obj = {}; // obj 被创建,初始计数为0

let a = obj; // obj 被引用,计数加1  1

let b = obj; // obj 被引用,计数加1  2

a = null; // obj 被释放,计数减1  1
b = null; // obj 被释放,计数减1  0   obj失活可以被回收

缺点:

标记-清除算法

标记不在使用的对象,然后清理这些对象,释放内存空间。分为两个阶段:

该算法解决了对象循环引用的问题

缺点:

标记-整理算法

标记-清除算法的改进,在清除之前进行内存整理。将内存中存活的对象移到内存的一端,使得在清除之后内存空间连续

该方法解决了标记-清除算法内存碎片化的问题,但仍然存在停顿的问题

v8 中的垃圾回收

v8 是什么?v8 是执行 javascript 的高性能引擎,由 Google 开发并开源。v8 引擎负责将 js 代码转变成计算机认识的机器代码。

分代式垃圾回收

v8 中采用的是分代式垃圾回收机制,根据对象的存活时间将内存划分为不同的中,分为新生代和老生代:

新生代垃圾回收

新生代垃圾回收是由副垃圾回收器负责

新生代垃圾回收机制是采用 Scavenge 算法来实现的:

新生代晋升老生代:

老生代垃圾回收

老生代垃圾回收是由主垃圾回收器负责

由于 Scavenge 算法在处理长时间存活和大规模对象存储时存在效率和内存利用率方面不足,所以老生代采用的是标记-清除和标记-整理算法

Orinoco 优化

Orinoco 是 v8 优化垃圾回收器的项目代号,为了优化用户体验,解决全停顿问题提出增量标记、三色标记法、惰性清理、并发、并行等优化方法

全停顿

js 代码运行在主线程上,当垃圾回收进行时会阻塞 js 代码的执行,垃圾回收完成后再恢复执行

由于新生代空间小,并且存活对象少,再配合 Scavenge 算法,停顿时间较短。但是老生代就不一样了,某些情况活动对象比较多的时候,停顿时间就会较长,使得页面出现了卡顿现象

并行垃圾回收

在垃圾回收器运行时,引入多个辅助线程来并行处理

并行垃圾回收示意图

增量垃圾回收

增量标记

将标记阶段分为多次阶段执行,中间穿插执行 js 代码。gc 和 js 代码交替执行,减少阻塞时间。

并行垃圾回收示意图

问题:
  1、如何做到垃圾回收器随时启动和暂停,重启如何恢复到上一步?
  2、标记好的数据在垃圾回收器暂停期间被修改了该如何处理?

针对上面两个问题,v8 引入三色标记法和写屏障

三色标记法

在之前老生代中使用的是标记-整理和标记-清除算法来执行垃圾回收,单纯的使用黑色和白色来进行标记,在垃圾回收之前会将所有对象设置成白色,然后从跟对象根据引用关系进行遍历,将访问到的对象设置成黑色,未访问到的对象就是白色,也就是需要被清除的对象

这种方法在增量标记的情况下垃圾回收器重启时内存中黑色白色都有,不知道上一步在哪里执行的

为此,v8 引入了三色标记: 白色,灰色,黑色:

引入灰色标记后,垃圾回收器重启就看有没有灰色标记,如果没有就说明已经完成了,如果有就从灰色开始继续执行

写屏障

根据三色标记法,如果对象在标记成黑色后,在垃圾回收器暂停期间新增加了一个白色节点,那么垃圾回收器不会把该节点标记成黑色,因为垃圾回收器已经走过这里了

为了解决这个问题,v8 引擎引入了写屏障:当黑色节点引用了白色节点,写屏障机制会强制将被引用的白色节点标记成灰色,这样在垃圾回收器重启时,会重新走这里进行标记。该方法也成为强三色不变性

惰性清理

v8 采用的是惰性清理,即在标记完成后延迟清理

并发垃圾回收

并行垃圾回收和增量垃圾回收依然会阻塞主线程,并发垃圾回收不会

并发垃圾回收:js 在主线程中执行,引入多个辅助线程进行垃圾回收

该方法同样面临增量回收的第 2 个问题:在垃圾回收的过程中,主线程 js 代码修改了引用关系,同样需要写屏障机制

并发垃圾回收

v8 优化总结

副垃圾回收器(新生代)

采用并行垃圾回收机制,同时启动多个辅线程进行垃圾回收

主垃圾回收器(老生代)

主垃圾回收器(老生代)

参考文献: