V8垃圾回收的优化历程

954

V8垃圾回收的优化历程

四个主要的垃圾回收任务

  • 新生代的回收工作通常很快(具体流程可康我上一篇文章)

  • 由增量标记器执行的标记步骤,根据数据大小而任意延长

  • 完整的垃圾收集,可能需要很长时间

  • 内存压缩的完整垃圾回收。这需要很长时间,但是会清理内存碎片提高内存可用率

增量回收

空闲任务

从Chrome 41开始,V8利用了一种新技术。将昂贵的垃圾回收内存操作分割为很小的空闲任务,并实现暂停和重启,来提升web的响应能力,减小Jank。

Chrome 41为Blink渲染引擎提供了一个任务调度程序,可以根据系统的繁忙程度和任务的紧急程度进行优先级排序,以确保Chrome保持快速响应。

空闲时间

以60HZ的显示器为例,使Chrome有大约16.6ms的时间来执行一帧的更新。因此,前一帧显示完毕,Chrome就会立即开始当前帧的工作,为新帧执行输入动画帧渲染任务。

如果Chrome在不到16.6ms的时间内完成了所有这些工作,它在剩下的时间里就没有其他事情要做了。Chrome调度器使V8能够利用空闲时间去执行空闲任务

Jank

在Chrome中,我们努力提供流畅的每秒60帧(FPS)的视觉体验。尽管已经尝试将垃圾回收器分割为小块的空闲任务执行垃圾回收,但是更大对象的垃圾收集操作可能并且确实会在不可预测的事件发生。

Chrome在16.6ms的内无法完成所有工作那就没有空闲时间,但内存垃圾还是会积累。当垃圾达到阈值就会开始进行GC,占用主线程。

这就是Jank的来源之一。

另一个Jank来源是记录与跟踪Chrome和V8之间共享的对象的生命周期。

尽管Chrome和V8内存堆是不同的,但它们必须针对某些对象(例如DOM节点)进行同步,这些对象以Chrome的C ++代码实现,但可通过JavaScript访问。

V8创建了一个不透明的数据类型,称为句柄,它使Chrome浏览器可以在不了解数据结构的情况下操纵V8堆对象。 只要Chrome保持句柄的引用,V8的垃圾收集器就不会对该对象进行回收。

V8的垃圾收集

V8实现了两个垃圾收集器:

  • 新生代
  • 老生代

新生代

由于新生代相对较小(在V8中最高为16MB,还只有一半用于保存对象),因此它很快就会充满需要频繁回收。

在M62之前,V8使用了Cheney半空间复制垃圾收集器。

从M62开始,V8将用于收集新生代的默认算法切换为并行Scavenger 类似于Cheney半空间复制垃圾收集器,不同之处在于V8会使用多个线程同时标记回收,而不是只在主线程进行标记回收工作。

新生代垃圾回收的的三种算法:

  • 单线程CheneyM62之前使用的回收算法器
  • 并行Mark-Evacuate老生代的并线算法
  • 并行ScavengerM62之后使用的并行回收算法
单线程Cheney

在M62之前,V8使用的是Cheney的半空间复制算法,该算法非常适合单核执行和分代方案。

  • 清除程序将调用堆栈中的引用以及从老生代到新生代的引用视为Root
  • 会扫描这些Root并复制from-space中尚未复制到to-space的活动对象,将其复制到to-space中去
  • 从垃圾回收中两次幸存下来的活动对象被提升到老生代中

单线程Cheney算法最初设计时考虑到了最优的单核性能。从那时起,世界发生了变化。即使在低端的移动设备上,CPU内核也经常是很多的。更重要的是,这些内核通常都是正常运行的。为了充分利用这些内核,V8 s垃圾收集器的最后一个顺序组件Scavenger必须进行现代化改造。

并行Mark-Evacuate

我们尝试了基于V8完整的Mark-Sweep-Compact收集器的并行Mark-Evacuate算法。

该算法包括三个阶段:标记-复制-修改指针

为了处理内存碎片,新生代内存仍旧使用semispace的方式进行内存compact来回收内存空间。

标记

新生代内存初始由并行的方式进行标记

复制

标记完成后将活动对象并行的地复制到对应的空间内。这个工作会分布式地在逻辑内存页上进行,这就产生了内存碎片。 每个线程都专注于复制他们自己的线程标记的活动对象。

修改指针

拷贝完成后,同样的并行操作模式会应用在更新指针上。

并行Scavenger

并行Scavenger合并了标记-复制-修改指针操作,该版本类似于Halstead的半空间收集器,不同之处在于V8使用多线程和负载平衡机制来扫描Root(增量回收)。

三种算法的总结:

  • 单线程Cheney算法最初设计时考虑到了最优的单核性能。但从那时起,时代变了。即使在低端的移动设备上,CPU内核也是有很大提升。更重要的是,这些内核通常都是正常运行的。为了充分利用这些内核,V8垃圾收集器必须进行现代化改造。

  • 并行标记最大优点是可以确切标记的活动对象。可以通过移动和重链接包含活动对象的页面来避免复制,这些活动对象也是由完整的Mark-Sweep-Compact收集器执行的。然而,在实践中,这在综合基准测试中很常见,很少出现在真正的网站上。并行并行标记的缺点是执行三个单独的锁步阶段的开销。当在堆上调用垃圾收集器时,这种开销尤其明显,堆上的对象大多是死对象,这是许多实际web页面的情况。

  • 并行Scavenger通过在小堆或几乎空堆上提供接近于优化的Cheney算法的性能,从而弥补这个性能差距,同时仍然提供高吞吐量,以防止堆在大量活动对象的情况下变得更大。

Orinoco项目

在过去的几年中,V8垃圾收集器(GC)已经发生了很大的变化。Orinoco项目采用了一个顺序、可暂停的垃圾收集器,并将其转换为一个主要是并行和并发的、带有增量回退的收集器。

任何垃圾收集器都有一些必须周期性执行的基本任务

  • 标记对象,确定是否是活动对象
  • 回收/重用非活动对象所占用的内存
  • 整理内存,清理内存碎片(可选的,不同垃圾处理器处理方式不同)

这些任务可以按顺序执行,也可以任意交错执行。一种直接的方法是暂停JavaScript执行并在主线程上依次执行这些任务。这可能会在主线程上导致jank和延迟问题,以及降低程序吞吐量。

V8中的两个垃圾收集器,主GC(标记-压缩)从整个堆也就是老生代开始收集垃圾。次要的GC(并行Scavenger)在新生代中收集垃圾。

这些算法和优化在垃圾收集文献中很常见,可以在许多垃圾收集语言中找到。但是,先进的垃圾收集技术已经取得了长足的进步。垃圾收集时间的一个重要指标是执行GC时主线程暂停的时间量。对于传统的垃圾收集器来说,这段时间可能会累积起来,而这段时间用于执行GC会直接影响用户体验,造成页面混乱呈现延迟较差。

接下来就是Orinoco的优化

平行

并行是主线程和其他线程在同一时间做大致相同的工作。这仍然是一种完全停止的方法,但是总的暂停时间现在除以参与的线程数(加上一些同步开销)。这是三种技术中最简单的一种。

增量

增量是利用主线程的空闲事件执行空闲任务。我们不会在增量会暂停整个GC,而只是GC所需工作总量的一小部分。这很困难,是三个技术中最困难的一个,因为JavaScript在每个增量工作段之间执行,这意味着之前处理的数据状态发生了变化,这可能会使以前增量完成的工作失效。从图中可以看出,这并没有减少在主线程上花费的时间(实际上,它通常会稍微增加),只是随时间扩展而已。这仍然是一个很好的技术。

利用平行技术,在JavaScript执行开始时并行开始标记工作。这无需占用主线程的时间,但是同样堆内存发生了变化,那之前的标记工作可能就白费了。

垃圾回收时会利用增量来分割回收和整理内存碎片的工作,利用空闲事件主线程与辅助线程一同操作。但增量的暂停、重启这也两个问题:

  • 在暂停期间,如果堆内存发生了变化新增数据并未标记那该如何处理新增的数据
  • 主线程和辅助线程可能同时访问操作同一对象,这就需要加一个访问锁

写屏障

这块我没找到V8具体的文档,就用GO里的了。我看了原理一下差不多。

写屏障解决了增量暂停、重启GC带来的问题。一个在并发垃圾回收器中才会出现的概念,垃圾回收器的正确性体现在:不应出现对象的丢失,也不应错误的回收还不需要回收的对象。

三色标记法

从垃圾回收器的视角来看,三色标记法规定了三种不同类型的对象,并用不同的颜色相称:

  • 白色对象(可能死亡):未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。
  • 灰色对象(暂停):已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。
  • 黑色对象(确定存活):已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。

V8使用强三色,不会有黑色对象指向白色对象

var obj={a:{b:{c:1}}}
obj.a.b = {d:233}
此时修改了a的引用,且{d:233}还是白色

这是将{d:233}标记为灰色,GC就能根据灰色内存进行遍历,不会遗漏任何一个对象了。({c:1}会在下一次垃圾回收被回收)

问题

强三色对回收周期的结束产生影响:

  • 如果某种并发回收器允许灰色赋值器的存在,则必须在回收结束之前重新扫描对象图。
  • 如果重新扫描过程中发现了新的灰色或白色对象,回收器还需要对新发现的对象进行追踪,但是在新追踪的过程中,赋值器仍然可能在其根中插入新的非黑色的引用,如此往复,直到重新扫描过程中没有发现新的白色或灰色对象。

这大大增加了标记的工作,但也解决了并行增量带来的问题

参考资料

增量回收 v8.dev/blog/free-g…

新生代的垃圾回收 v8.dev/blog/orinoc…

Orinoco v8.dev/blog/trash-…

写屏障 www.bookstack.cn/read/qcrao-…

极客时间的《图解V8》