Skip to content

浏览器垃圾回收

2024-06-29T00:00:00.000Z

什么是垃圾回收

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

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

垃圾回收算法

引用计数法

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

  • 对象创建时,创建初始计数为 0
  • 对象被引用,计数加 1
  • 对象被释放,计数减 1
  • 对象的计数为 0,对象被回收
typescript
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失活可以被回收

缺点:

  • 无法处理循环引用:当存在循环引用时,计数永远不会为 0,对象永远不会被回收,就会导致内存泄漏

标记-清除算法

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

  • 标记
    从根节点开始,递归遍历所有对象的引用关系,将被访问到的对象打上标记,表示对象是存活的
  • 清除
    遍历整个内存,对于未打上标记的对象,释放内存空间

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

缺点:

  • 阻塞(停顿):js 是单线程的,在垃圾回收器执行标记清除时程序会暂停运行,直到垃圾回收完成。当程序复杂时,可能会发生明显的卡顿。
  • 内存碎片化:标记-清除算法会在清理垃圾对象时,不会整理内存空间,产生大量不连续的内存碎片。这可能导致后续的内存分配难以找到足够大的连续内存块,从而使得内存的利用率降低

标记-整理算法

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

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

v8 中的垃圾回收

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

分代式垃圾回收

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

  • 新生代:
    • 存活时间较短的对象(经历一次回收就释放)
    • 采用 Scavenge 算法的快速垃圾回收策略
  • 老生代:
    • 存活时间较长的对象(经历多次回收依旧存活)
    • 采用标记-整理和标记-清除算法

新生代垃圾回收

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

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

  • 将新生代内存划分为两个等大的空间:from 空间和 to 空间
  • 新对象首先进入 from 空间,当 from 空间内存满了的时候就会进行垃圾回收:
    • 标记阶段:从根对象开始通过引用关系遍历标记所有活动的对象
    • 复制阶段:将所有标记的对象复制到 to 空间并排序,使得 to 空间内存连续
    • 清除阶段:清除 from 空间内非活动对象
    • 空间交换:将 from 空间和 to 空间置换,即 from 空间变成 to 空间,to 空间变成 from 空间

新生代晋升老生代:

  • 存活时间达到阈值:每个对象都有一个计数器,经历一次回收依旧存活计数器加 1,当计数器达到阈值时就晋升到老生代
  • to 空间内存占比达到一定比例:to 空间内存占比达到一定比例(一般是 25%~50%)也会触发对象晋升,这是避免新生代内存被占满频繁的触发内存回收

老生代垃圾回收

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

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

Orinoco 优化

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

全停顿

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

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

并行垃圾回收

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

并行垃圾回收示意图

增量垃圾回收

增量标记

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

并行垃圾回收示意图

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

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

三色标记法

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

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

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

  • 白色:表示对象未被访问过,可以被回收
  • 灰色:表示对象被访问过,但其引用还未被访问(即正在处理该对象)
  • 黑色:表示对象被访问过,且被标记

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

  • 从根对象开始
    三色标记法-从根对象开始
  • 将访问到的对象标记为黑色
    三色标记法-将访问到的对象标记为黑色
  • 最终形态
    三色标记法-最终形态

写屏障

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

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

惰性清理

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

并发垃圾回收

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

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

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

并发垃圾回收

v8 优化总结

副垃圾回收器(新生代)

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

主垃圾回收器(老生代)

  • 2011 年 v8 从全停顿切换到增量标记;2018 年以后 gc 技术有了重大突破,这项技术名为并发标记
  • 这三种技术各有优缺点,v8 老生代垃圾回收就融合了这三种技术:
    • 主垃圾回收器主要使用并发标记,在主线程开始运行 js 代码时,启动多个辅线程进行标记
    • 标记完成后使用并行整理和清理操作
    • 同时也采用了增量回收的方式,在整理和清除操作中穿插了 js 执行

主垃圾回收器(老生代)

参考文献:

最后更新于: