性能文章>深入了解 Chrome V8 javascript engine 的垃圾收集引擎>

深入了解 Chrome V8 javascript engine 的垃圾收集引擎原创

5月前
210311

JavaScript 性能仍然是 Chrome 价值观的关键方面之一,尤其是在实现流畅体验方面。 从 Chrome 41 开始,V8 利用一种新技术来提高 Web 应用程序的响应能力,方法是将昂贵的内存管理操作隐藏在小的、否则未使用的空闲时间块中。 因此,Web 开发人员应该期待更流畅的滚动和黄油动画(指像黄油一样丝滑的动画),并且由于垃圾收集而大大减少了卡顿。

 

许多现代语言引擎,例如 Chrome 的 V8 JavaScript 引擎,动态管理运行应用程序的内存,因此开发人员无需自己担心。 引擎定期传递分配给应用程序的内存,确定不再需要哪些数据,并将其清除以释放空间。 此过程称为垃圾收集。

 

在 Chrome 中,我们努力提供流畅的每秒 60 帧 (FPS) 的视觉体验。 尽管 V8 已经尝试在小块中执行垃圾收集,但更大的垃圾收集操作可能而且确实会在不可预测的时间发生——有时是在动画中间——暂停执行并阻止 Chrome 达到 60 FPS 的目标。

 

Chrome 41 包括一个用于 Blink 渲染引擎的任务调度程序,它可以对延迟敏感的任务进行优先级排序,以确保 Chrome 保持响应和敏捷。 除了能够确定工作的优先级之外,该任务调度程序还集中了解系统的繁忙程度、需要执行的任务以及这些任务的紧迫程度。 因此,它可以估计 Chrome 何时可能处于空闲状态,以及它预计会保持空闲多长时间。

 

当 Chrome 在网页上显示动画时,就会出现这种情况。 动画将以 60 FPS 的速度更新屏幕,给 Chrome 大约 16.6 毫秒的时间来执行更新。 因此,Chrome 将在前一帧显示后立即开始处理当前帧,为这个新帧执行输入、动画和帧渲染任务。 如果 Chrome 在不到 16.6 毫秒的时间内完成了所有这些工作,那么在需要开始渲染下一帧之前,它就没有其他事情可做。 Chrome 的调度程序使 V8 能够在 Chrome 空闲时通过调度特殊的空闲任务来利用这个空闲时间段。

带有空闲任务的帧渲染 - 深入了解 Chrome V8 javascript engine 的垃圾收集引擎 - HeapDump性能社区
图 1:带有空闲任务的帧渲染



空闲任务是特殊的低优先级任务,当调度程序确定它处于空闲期时运行。 空闲任务有一个截止日期,这是调度程序对它期望保持空闲状态的估计。 在图 1 的动画示例中,这将是开始绘制下一帧的时间。 在其他情况下(例如,当没有发生屏幕上的活动时),这可能是计划运行下一个待处理任务的时间,上限为 50 毫秒,以确保 Chrome 对意外的用户输入保持响应。 空闲任务使用截止日期来估计它可以做多少工作而不会导致输入响应出现卡顿或延迟。

 

在空闲任务中完成的垃圾收集对关键的、延迟敏感的操作是隐蔽的。 这意味着这些垃圾收集任务是“免费”完成的。 为了了解 V8 是如何做到这一点的,有必要回顾一下 V8 目前的垃圾回收策略。

 

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

V8 使用分代垃圾收集器,将 Javascript 堆拆分为用于新分配对象的小型年轻代和用于长期存活对象的大型老年代。 由于大多数对象在年轻时死亡,这种分代策略使垃圾收集器能够在较小的年轻代(称为清除)中执行常规的、短时间的垃圾收集,而无需跟踪年老代中的对象。

 

注:什么是分代垃圾收集?

分代垃圾收集是利用代际假设的跟踪垃圾收集。 对象世代相传。 新对象分配在新生代中,如果它们存活,则晋升到老年代。 老年代中的对象被谴责的频率较低以节省 CPU 时间。

一个对象通常很少引用年轻代的对象。 因此,一代中的对象通常也是很少引用年轻代中的对象。 这意味着在收集年轻代的过程中对老年代的扫描可以通过记忆集来更有效地完成。
在一些纯函数式语言(即没有更新)中,所有引用都是按时间倒退的,在这种情况下,记忆集是不必要的。

 

年轻代使用半空间(semi-space)分配策略,新对象最初分配在年轻代的活动半空间中。 一旦该半空间变满,清除操作会将活动对象移动到另一个半空间。 已经移动过一次的对象被提升到老年代,被认为是长寿命(long-living)的。 一旦活动对象被移动,新的半空间将变为活动状态,并且旧半空间中任何剩余的死对象都将被丢弃。

 

因此,年轻代清除的持续时间取决于年轻代中活动对象的大小。 当大多数对象在年轻代中变得无法访问时,清除将很快(<1 ms)。 但是,如果大多数对象在清除过程中幸存下来,清除的持续时间可能会显着延长。

 

当老年代中活动对象的大小超过启发式推导(heuristically-derived)的限制时,将执行整个堆的主要收集。 老一代使用带有多项优化的标记和清除收集器(mark-and-sweep)来改善延迟和内存消耗。 标记延迟取决于必须标记的活动对象的数量,对于大型 Web 应用程序,标记整个堆可能需要超过 100 毫秒。 为了避免主线程长时间暂停,V8 早就有能力以许多小步骤增量标记活动对象,旨在将每个标记步骤的持续时间保持在 5 毫秒以下。

 

标记后,通过清扫整个老一代内存,空闲内存再次可供应用程序使用。 此任务由专用的清扫线程同时执行。 最后,执行内存压缩以减少老年代的内存碎片。 此任务可能非常耗时,并且仅在存在内存碎片问题时才执行。

 

总结起来,有四个主要的垃圾收集任务:

 

  • 年轻代的清除,通常很快
  • 增量标记执行的标记步骤,可以任意长,具体取决于步长
  • 完整的垃圾回收,可能需要很长时间
  • 具有积极内存压缩的完整垃圾收集,可能需要很长时间,但会清理碎片内存

 

为了在空闲期间执行这些操作,V8 将垃圾收集空闲任务发布到调度程序。 当这些空闲任务运行时,它们被提供了它们应该完成的最后期限。 V8 的垃圾收集空闲时间处理程序评估应该执行哪些垃圾收集任务以减少内存消耗,同时遵守最后期限以避免未来帧渲染或输入延迟的卡顿。

 

如果应用程序测量的分配率显示年轻代可能在下一个预期空闲期之前已满,垃圾收集器将在空闲任务期间执行年轻代清理。 此外,它会计算最近清理任务所花费的平均时间,以预测未来清理的持续时间并确保它不会违反空闲任务的截止日期。

 

当老年代存活对象的大小接近堆限制时,开始增量标记。 增量标记步骤可以按应标记的字节数线性缩放。 根据测量的平均标记速度,垃圾收集空闲时间处理程序尝试将尽可能多的标记工作放入给定的空闲任务中。

 

如果老年代快满了,并且如果提供给任务的截止时间估计足够长以完成收集,则在空闲任务期间安排完整的垃圾收集。 收集暂停时间是根据标记速度乘以分配对象的数量来预测的。 只有当网页空闲很长时间时,才会执行带有额外压缩的完整垃圾回收。

 

性能评估

 

为了评估在空闲时间运行垃圾收集的影响,我们使用 Chrome 的遥测性能基准测试框架来评估流行网站在加载时滚动的流畅程度。 我们对 Linux 工作站上的前 25 个站点以及 Android Nexus 6 智能手机上的典型移动站点进行了基准测试,这两个站点都可以打开流行的网页(包括 Gmail、Google Docs 和 YouTube 等复杂的网络应用程序)并在几秒钟内滚动其内容 . Chrome 旨在保持 60 FPS 的滚动速度,以获得流畅的用户体验。

 

图 2 显示了在空闲时间安排的垃圾收集的百分比。 与 Nexus 6 相比,工作站更快的硬件导致了更多的总体空闲时间,从而能够在此空闲时间安排更大比例的垃圾收集(43% 相比 Nexus 6 上的 31%),从而提高了约 7% 我们的 jank 指标。

空闲时间发生的垃圾回收百分比 - 深入了解 Chrome V8 javascript engine 的垃圾收集引擎 - HeapDump性能社区
图 2:空闲时间发生的垃圾回收百分比



除了提高页面渲染的平滑度之外,这些空闲时间段还提供了在页面完全空闲时执行更积极的垃圾收集的机会。 Chrome 45 的最新改进利用这一点来大幅减少空闲前台选项卡消耗的内存量。 下面这个视频内容中介绍了与 Chrome 43 中的相同页面相比,Gmail 的 JavaScript 堆在空闲时的内存使用量如何减少约 45%。


Chrome 45(左)与 Chrome 43 上 Gmail 的内存使用情况:https://youtu.be/ij-AFUfqFdI



这些改进表明,可以通过更智能地了解何时执行昂贵的垃圾收集操作来隐藏垃圾收集暂停。 Web 开发人员不再需要担心垃圾收集暂停,即使是针对如丝般流畅的 60 FPS 动画也是如此。 随着我们推动垃圾收集调度的界限,请继续关注更多改进。

 

相关阅读

Chrome V8 javascript engine C++ 高性能垃圾收集器 Oilpan

Chrome V8 javascript engine 的3种垃圾回收算法

点赞收藏
分类:标签:
风之石
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

七张图,让你看懂Go语言的垃圾回收原理

七张图,让你看懂Go语言的垃圾回收原理

JVM系列第8讲:JVM 垃圾回收机制

JVM系列第8讲:JVM 垃圾回收机制

JVM系列第10讲:垃圾回收的几种类型

JVM系列第10讲:垃圾回收的几种类型

1
1