写点什么

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 读者交流群)。

2015 年 8 月 25 日 19:023945
用户头像

发布了 268 篇内容, 共 101.4 次阅读, 收获喜欢 17 次。

关注

评论

发布
暂无评论
发现更多内容

SpringCloud 微服务实现数据权限控制

Barry的异想世界

SpringCloud SpringBoot 2 权限认证 数据权限

拍乐云语音聊天室SDK,助力非洲版陌陌“Mochat”打造粉丝经济

拍乐云Pano

音视频 RTC 语音聊天室 出海社交 社交泛娱乐

一名分布式存储工程师的技能树是怎样的?

焱融科技

分布式 存储 分布式存储 分布式文件

网络请求是如何发送出去的

kof11321

网络

你真的会正确使用日志吗?

xcbeyond

Java 代码规范 28天写作 日志级别

Elasticsearch 的正式介绍

escray

elasticsearch elastic 28天写作 死磕Elasticsearch 60天通过Elastic认证考试

从标准到开发,解读基于MOF的应用模型管理

华为云开发者社区

模型 ROMA 应用模型 mof

智能停车管理系统搭建,智慧小区智能化解决方案

t13823115967

智慧小区

Java 程序经验小结:剖析方法重载

后台技术汇

28天写作

如何轻松简便地在电脑上制作视频&数据更新 | 视频号 28 天 (04)

赵新龙

28天写作

十八般武艺玩转GaussDB(DWS)性能调优:SQL改写

华为云开发者社区

数据库 sql 性能优化 GaussDB 算子

开发效率提升15倍!批流融合实时平台在好未来的应用实践

Apache Flink

流计算 fink

软件测试--数据库基础知识

测试人生路

数据库 软件测试

STM32标准库开发实战指南

华为云开发者社区

SMT32处理器 stm32 内核 寄存器

探索 Vue.js 响应式原理

pingan8787

vue.js Vue 前端 响应式 28天写作

HDFS SHELL详解(4)

罗小龙

hadoop 28天写作 hdfs shell

让机器有温度:带你了解文本情感分析的两种模型

华为云开发者社区

自然语言处理 神经网络 机器学习 深度学习

【JS】防止浏览器控制台被直接查看(1)

学习委员

JavaScript 前端 js 28天写作

人与人需要保持边界

熊斌

读书笔记 成长笔记 28天写作

《破壁MySQL》 - MySQL概述

haxianhe

MySQL 破壁MySQL 破壁

IndexedDB详解

程序那些事

程序那些事 indexedDB webtech 浏览器技术 前端技术

第十二周课后练习

dll

什么是ReadWriteMany?

焱融科技

Kubernetes 云原生 存储 焱融科技 持久化存储

8. 格式化器大一统 -- Spring的Formatter抽象

YourBatman

Spring Framework Converter Formatter

springboot多模块配置问题

原来不悔

springboot Spring Frame

Java 并发编程之 JMM & volatile 详解

vivo互联网技术

Java volatile JMM 指令重排序

DAPP软件开发|DAPP系统APP开发

开發I852946OIIO

系统开发

第十三周课后作业

dll

握草,这些研发事故30%我都干过!

小傅哥

Java 小傅哥 28天写作 线上事故 系统秒杀

人脸识别门禁系统搭建,智慧小区实施方案

t13823115967

智慧平安小区平台开发

公安合成作战指挥中心系统开发解决方案,智慧警务平台搭建

WX13823153201

智慧警务平台搭建

Google V8的垃圾回收引擎-InfoQ