V8 垃圾回收机制

1. 什么是垃圾

垃圾通常是指非活动对象:即不再使用的对象

let obj = { name: 'obj1' }
obj = { name: 'obj2' } // 当该行代码执行完毕后,由于 { name: 'obj1' } 并不会被使用,故而认定其为非活动对象,会被垃圾回收器回收

2. 垃圾回收算法

目前 V8 采用可访问性(reachability)算法来判断是否为活动对象

  1. 标记非活动对象
    1. 从 GC Root 出发,遍历 GC Root 中所有的对象
    2. 能被遍历到的对象被认定为活动对象
    3. 否则被认定为非活动对象
  2. 清理所有的非活动对象
  3. 内存整理:一般情况下,经过频繁的垃圾回收后,会产生大量的内存碎片。此时如果想要分配一块连续较大的空间,就很有可能会面临内存空间不足的情况:解决方法就是进行内存整理,腾出足够的连续内存空间
    • 内存整理之前/后
    • 内存整理之后

2.1 代际假说

代际假说有两个特点:

  1. 大部分的对象在内存中的存活时间很短, 如自己创建的一些对象, 变量
  2. 存活超过一定时间的对象很可能会活得更久, 如: window, document

介于代际假说,V8 采用了两种垃圾回收器: 副垃圾回收器负责对新生区进行垃圾回收, 而主垃圾回收器负责对老生区进行垃圾回收

2.2 副垃圾回收器

副垃圾回收器是专门用来处理存活时间较短的对象的, 其支持1~8M的容量(该空间被称为新生区), 并将新生区分为对象区和空闲区两个区域, 且新创建的对象都会被分配到对象区中

2.2.1 Scanvenge 算法的执行

副垃圾回收器使用的是 Scanvenge 算法. 在执行 Scanvenge 算法时, 其有如下的步骤

  • 标记对象区中的活跃对象
  • 将对象区中的活跃对象有序地复制排列到空闲区中
  • 令空闲区和对象区角色互换

2.2.2 对象晋升

当新生区中的对象经过两次垃圾回收后仍没有被清除, 介于代际假说, 判定其会生存更长的时间, 故而将其移动至老生区中

2.3 主垃圾回收器

2.3.1 垃圾回收算法

主垃圾回收器组合采用的垃圾回收算法为:

  1. 标记-清除算法
    1. 主垃圾回收器会通过对 GC Root 的遍历, 对活动对象和非活动对象进行标记
    2. 直接对非活动对象进行清除
  2. 标记-整理算法: 在进行多次标记-清除算法后, 难以避免会产生一定数量的内存碎片. 通过标记-整理算法可以对内存进行整理, 从而清除内存碎片
    1. 通过对 GC Root 的遍历, 对活动对象和非活动对象进行标记
    2. 让活动对象向一端移动, 形成一块连续的, 由活动对象占据的内存空间
    3. 直接清理掉活动对象一端之外的其他内存空间

缺点: 由于 JS 代码是执行在主线程之上的, 当需要执行垃圾回收算法时, JS 脚本会停止执行, 待垃圾回收算法执行完毕之后, 才会重新继续运行. 这种行为被称作全停顿(Stop-The-World)
如下图所示, 主线程会有 200ms 的时间用来进行垃圾回收, 在这个过程中, 任何 JS 代码都无法执行, 页面陷入卡顿状态, 会造成不良的用户体验

2.3.2 全停顿的优化

2.3.2.1 并行回收

在进行垃圾回收时, 不仅仅只有主线程在运作, 还有多个辅助线程帮助进行标记和清理, 从而使得执行垃圾回收算法所需要的时间大大减少(V8 副垃圾回收器采用的就是这样的策略)

2.3.2.2 增量回收

但是并行回收仍然会造成全停顿, 且老生区的大对象很多, 即使有辅助线程仍然也需要很久的时间.

对此, V8 引入了增量回收策略: 将垃圾回收算法切割为很多小段进行执行, 每次只执行一小段, 这样就不会发生长时间的停顿从而影响用户体验了

要实现增量回收, 需要满足以下两点要求:

  • 垃圾回收可以被随时暂停和启动, 暂停时需要保存当时的状态, 等待下一次垃圾回收时启用
    • 三色标记: 对于增量回收, 其在遍历 GC Root 时
      • 如果一个节点和它的子节点都能被 GC Root 引用到, 则其被标记为黑色
      • 如果一个节点能被 GC Root 引用到, 而其子节点还没有被标记, 则将该节点被标记为灰色, 用来表示该节点是"正在处理阶段"
      • 如果一个节点无法被 GC Root 引用到, 则默认其为白色
    • 引入三色标记后, 通过查看灰色节点的存在与否就能够知道标记阶段是否完成
      • 如果没有灰色节点, 则标记完成. 可以开始进行清理工作: 将白色节点全部回收
      • 如果存在灰色节点, 则从灰色节点开始, 继续进行标记工作
  • 在暂停期间, 如果被标记的垃圾数据被 JS 代码修改了, 那么垃圾回收器需要能够正确地处理
    • 写屏障(Write-barrier): 如果一个黑色节点的子节点为白色, 则自动将白色转换为灰色
      • 标记结束:
      • JS 代码的修改:
      • 写屏障:

但是对内存压力较大的堆, 垃圾回收器仍然可能出现长时间的暂停来维持分配

2.3.2.3 并发标记

并发标记主要发生在辅助线程上. 当并发标记正在进行时, 主线程可以继续运行

2.3.3 主垃圾回收器的垃圾回收过程

posted @ 2022-11-16 16:05  小阁下  阅读(138)  评论(0编辑  收藏  举报