Google V8 的垃圾回收引擎

  • 张天雷

2015 年 8 月 25 日

话题:语言 & 开发架构

Google V8 引擎(以下简称 V8)是 Google 的一个开源项目,旨在构建一个高效的 JavaScript 引擎,是 Google 特别为 Chrome 高速运行网页应用(Web App)而开发的。同时,它可以作为一个独立的库被嵌入到其他应用程序中,以提高软件的灵活性和可扩展性。目前,V8 引擎由于其高效的性能吸引了越来越多的关注。

Google 的好几款应用都是基于 JavaScript,其中包括 Gmail 电子邮件服务、Google Maps 地图数据服务、以及 Google Docs office 套件。这些应用表现出的速度不仅受到服务器、网络、渲染引擎(Rendering Engine)等因素的影响,同时也受到 JavaScript 本身执行速度的影响。而 Google 研发的 V8 JavaScript 引擎通过采取一系列关键技术,大大提升了 JavaScript 的执行速度,关键技术包括 JIT 编译 (JIT Compile)、垃圾回收(Garbage Collection)、内嵌缓存(Inline Cache)、隐藏类等。在本文中,重点对 V8 的垃圾回收引擎进行简单介绍。

什么是垃圾回收

JavaScript 的性能是关系到 Chrome 价值的一个重要方面,因为它涉及到用户能否获得一个流畅的使用体验。从 Chrome41 版本开始,通过在一些小的、零散的空闲时间内执行昂贵的内存管理操作,V8 提高了 Web 应用程序的响应能力。

许多脚本语言引擎,如 V8 引擎,对运行的应用程序实施动态的内存管理。引擎可以定期检查分配给应用程序的内存,确定哪些数据不再需要,并清除出来,以腾出内存空间。这个过程被称为垃圾回收。垃圾回收可以大幅简化程序的内存管理代码,降低程序员的负担,减少因长时间运转而带来的内存泄露问题。

什么时候执行垃圾回收

Chrome 41 版本包括了一个针对渲染引擎的任务调度器(Task Scheduler),以确保 Chrome 浏览器一直保持响应和流畅,任务调度器使延迟敏感的任务拥有更高的优先级。为了实现这一目标,任务调度器需要获取多种信息,包括系统的繁忙程度,哪些任务需要被执行,以及这些任务的紧迫程度。在此基础上,任务调度器可以评估 Chrome 什么时候可能空闲,以及预计会空闲多久。

举一个简单的例子,当 Chrome 在网页上播放一段视频的时候。视频在屏幕上的更新速率为 60 帧每秒(FPS),即 Chrome 大概每次有 16.6ms 的时间来进行更新。这样,Chrome 将在前一帧显示后立刻启动当前帧的工作,为当前帧执行输入和渲染任务。如果 Chrome 完成所有这些工作用时不到 16.6ms,在剩下的时间内, Chrome 浏览器处于闲置状态。此时,调度器通过调度一些特殊的空闲任务(Idle Tasks)可以使 Chrome 能够利用这些空闲时间。如下图所示。

空闲任务是一些特殊的低优先级任务,它们在调度器确定 Chrome 空闲的时候才被运行。空闲任务拥有一个截止时间,截止时间是调度器估计 Chrome 能够保持空闲的时间。例如,在视频播放的例子中,截止时间是下一帧应该开始的时间。在其他情况下,截止时间可能是下一个待处理任务计划运行的时间,通常其有一个 50ms 的上限,以确保 Chrome 浏览器对突然的用户输入仍能保持响应。空闲任务的截止时间能够被用来估算在不会造成用户输入响应延迟的情况下 Chrome 可以完成的工作量。

垃圾回收就是一种典型空闲任务,其隐藏在一些关键的、延迟敏感的任务背后。这意味着这些垃圾回收任务是在没有影响用户体验的情况下,在 Chrome 的空闲时间内被执行。为了理解 V8 是如何做到这一点,下面我们对 V8 目前的垃圾回收策略进行深入了解。

深入了解 V8 的垃圾回收引擎

V8 采用了一个分代(Generational)垃圾回收器,将内存堆分割为新生代(Young Generation)和老生代(Old Generation)。新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。由于绝大多数对象的生存期很短,只有少数对象的生存期较长,这种分代策略能使垃圾回收器对新生代对象执行一些规则的、小的垃圾回收(被称为 Scavenge)。V8 分别对新生代对象和老生代对象使用不同的垃圾回收算法来提升垃圾回收的效率。

对象起初会被分配在新生代内存区(通常很小,只有 1-8 MB,具体根据任务分配)。大多数的对象被分配在这里,这个区域很小但是垃圾回收特别频繁。新生代使用半空间(Semi-space)分配策略,其中新对象最初分配在新生代的活跃半空间内。一旦半空间已满,一个 Scavenge 操作将活跃对象移出到其他半空间中,被认为是长期驻存的对象,并被晋升为老生代。一旦活跃对象已被移出,则在旧的半空间中剩下的任何死亡对象被丢弃。

因此新生代对象的 Scavenge 操作的持续时间取决于新生代中活跃对象的数量。在大部分新生代对象活跃时间不长的情况下,一个 Scavenge 操作非常快(<1ms)。然而,如果大多数对象都需要被 Scavenge 的时候,Scavenge 操作的持续时间显然会更长。

Scavenge 操作对于快速回收、紧缩小片内存效果很好,但对于大片内存则消耗过大。因为 Scavenge 操作需要出区和入区两个区域,这对于小片内存尚可,而对于超过数 MB 的内存就开始变得不切实际了。老生代所保存的对象大多数是生存周期很长的甚至是常驻内存的对象,而且老生代占用的内存较多,通常包含有上百 MB 的数据。因此,V8 在老生代中的垃圾回收采用标记 - 清除(Mark-Sweep)和 Mark-Compact 相结合的策略。

当老生代中的活动对象增长超过了一个预设的限制的时候,将对堆栈执行一个大回收。老生代垃圾回收使用 Mark-Sweep 策略,其采用了几种优化方法来改善延迟和内存消耗。标记时间取决于必须标记的活跃对象的数目,对于一个大的 web 应用,整个堆栈的标记可能需要超过 100ms。由于全停顿会造成了浏览器一段时间无响应,所以 V8 使用了一种增量标记的方式标记活跃对象,将完整的标记拆分成很多小的步骤,每做完一部分就停下来,让 JavaScript 的应用线程执行一会,这样垃圾回收与应用线程交替执行。V8 可以让每个标记步骤的持续时间低于 5ms。

由于标记完成后,所有对象都已经被标记,即不是活跃对象就是死亡对象,堆上有多少空间已经确定。清除时,垃圾回收器会扫描连续存放的死对象,将其变成空闲空间。这个任务是由专门的清扫线程同步执行。最后,为减少老生代对象产生的内存碎片,还要执行内存紧缩(Memory Compaction)。这个任务可能是非常耗时的,并且仅当内存碎片成为问题的时候才进行。

总之,有四个主要的垃圾回收任务:

  1. 新生代对象的 Scavenge,这通常是快速的;

  2. 通过增量方式的标记步骤,依赖于需要标记的对象数量,时间可以任意长;

  3. 完整垃圾回收,这可能需要很长的时间;

  4. 带内存紧缩的完整垃圾回收,这也可能需要很长的时间,需要进行内存紧缩。

为了在空闲时段执行这些操作,V8 给任务调度器公布垃圾回收空闲任务。当这些空闲任务运行时,它们被提供一个需要完成的截止时间。 V8 的垃圾回收空闲时间处理程序为了减少内存消耗,评估哪些垃圾回收任务应该被执行,同时紧盯截止时间以避免在帧渲染过程中出现用户输入响应延迟。

如果应用的内存分配率显示在下一个期待的空闲时间之前新生代内存区已经满了,垃圾回收器将执行新生代对象的 Scavenge 操作。此外,它还会计算最近的 Scavenge 操作所花费的平均时间,可以帮助预测未来 Scavenge 操作的持续时间,并确保它不会超出空闲任务的截止时间。

当老生代中活跃对象的数量接近堆栈限制的时候,增量标记开始。增量标记的步数与需要标记的字节数成线性比例。根据测得的平均标记速度,垃圾回收空闲时间处理程序尝试尽可能地为一个垃圾回收任务安排多的标记工作。

如果老生代内存区几乎满了,此外任务的截止时间足够长可以完成回收任务,在一个空闲任务中将调度一个完整的垃圾回收任务。回收任务的执行时间是标记速度乘以分配对象的数目。带内存紧缩的完整垃圾回收只有在 Chrome 空闲足够长的时间才被执行。

性能评价

为了评价空闲时间运行垃圾回收任务的影响,V8 使用 Chrome 的性能遥测基准框架,以评价加载热门网站时页面滚动的平滑度。选择 Linux 工作站上排名前 25 位的网站,以及 Android Nexus 6 智能手机上的一些典型的移动网站,在两种情况下打开流行的网页(包括一些复杂的 web 应用,如 Gmail,Google 文档和 YouTube),滚动其内容需要几秒钟。 为了保证流畅的用户体验,Chrome 的目标是滚动显示保持在 60 FPS。

下图显示了空闲时间垃圾回收的比例。相比 Nexus 6,工作站因为拥有更好的硬件,导致总体上拥有更多的空闲时间,从而导致其在空闲时间内拥有一个更高的垃圾回收比例(43%,而 Nexus6 为 31%),工作站的jank 指标比 Nexus 6 也高了 7%。

事实上,垃圾回收是一个复杂的过程。Google V8 的垃圾回收方法能够自动完成垃圾回收,大大减轻了应用开发者的负担,能够让他们集中精力于更重要的事情上。尽管目前 V8 的垃圾回收引擎并不完美,仍存在一些性能问题而且偶尔会出现奇怪的现象,但我们还是很高兴地看到其正在变得更好,Google 的工程师 Hannes Payer 和 Ross McIlroy 在其博客中说到,他们一直在努力对垃圾回收做更多的改进。


感谢郭蕾对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

语言 & 开发架构