not a better man

前端技术

闲聊v8垃圾回收器的演变历史(1)

javascript 是带有gc机制的语言,与c,c++ 这种需要用户手动进行内存的管理语言不同,带有gc机制的语言,我们在写代码的过程无需管理已分配的内存块,减少我们编写的代码心智成本。

chrome浏览器js引擎是v8, 起初v8的大部分垃圾回收工作是在主渲染线程上进行的。我们清楚当浏览器器在16.66ms内无法完成一帧的渲染时,那么就会出现页面卡顿现象。当GC需要维护的对象变多时,垃圾回收过程所占用的时间也会导致页面卡顿。如下图所示

垃圾回收工作在渲染线程中进行

为了减少GC时间,v8团队做了很多优化,例如在垃圾收集过程中,GC的大部分时间处理各种数据结构,但是这些数据结构中有许多与垃圾收集无关的。例如ArrayBuffer的列表,以及每个ArrayBuffer的视图列表。这些列表允许高效地实现DetachArrayBuffer操作,但是不会对访问ArrayBuffer视图造成任何性能冲击。然而,在网页创建了数百万个ArrayBuffer的情况下(例如,基于WebGL的游戏),在垃圾收集期间更新这些列表会导致严重的卡顿。v8团队在 Chrome 46 中,删除了这些列表,而是通过在每次加载和存储到 ArrayBuffers 之前插入检查来检测分离的缓冲区。这通过在程序执行过程中分散走大的记账列表来摊销成本,从而减少了垃圾。虽然理论上,每次访问检查会拖慢大量使用ArrayBuffers的程序的吞吐量,但实际上,V8的编译器通常可以删除多余的检查,并将剩余的检查从循环中提升出来,从而使执行曲线更加平滑,而整体性能几乎没有受到影响。

另一个垃圾标记是跟踪Chrome和V8之间共享对象的寿命相关的记账。虽然Chrome和V8的内存堆是不同的,但对于某些对象,如DOM节点,它们必须同步,这些对象是在Chrome的C++代码中实现的,但可以从JavaScript中访问。那时候V8引擎创建了一种叫做句柄的不透明数据类型,它允许Chrome在不知道任何实现细节的情况下操作V8堆对象。将对象的寿命与句柄绑定在一起:只要Chrome将句柄保存,V8的垃圾收集器就不会扔掉这个对象。V8为每个通过V8 API传回给Chrome的句柄创建了一个称为全局引用的内部数据结构,这些全局引用能告诉V8的垃圾收集器对应的对象仍然被程序使用。对于WebGL游戏来说,Chrome可能会创建数百万个这样的句柄,而V8则需要创建相应的全局引用来管理它们的生命周期。在主垃圾收集暂停中处理这些巨量的全局引用是可以观察到的jank。幸运的是,传达给WebGL的对象往往只是传递,而从未被实际修改,因此可以进行简单的静态逃逸分析。实质上,对于已知通常以小数组为参数的WebGL函数,底层数据会被复制到堆栈上,使得全局引用过时。这种混合方法的结果是,对于渲染量大的WebGL游戏,GC暂停时间最多可减少50%。

那个时候V8的大部分垃圾收集是在主渲染线程上进行的。将垃圾收集操作转移到并发线程上,可以减少垃圾收集器的等待时间,这是一个很复杂的任务,因为主JavaScript应用程序和垃圾收集器可能同时观察和修改相同的对象。在chrome46版本,并发还仅限于扫除老一代的常规对象JS堆。

虽然chrome团队做了以上的优化,但是尽量缩短gc在主线程(js执行线程)占用的时间,v8团队开始考虑gc的并行处理,于是开启了名为Orinoco GC 项目

发表评论