v8 引擎

定义

V8 是一个由 Google 开发的开源 JavaScript 引擎,目前用在 Chrome 浏览器和 Node.js 中,其核心功能是执行易于人类理解的 JavaScript 代码。

v8 架构

  1. 早期架构

源代码 => AST => 二进制代码

标记重复执行的函数,将标记的代码进行优化编译成效率更高的二进制代码,再次运行时使用效率更高的二进制代码。

  1. 早期架构的问题
  • 内存占用高 v8 执行的过程中,二进制代码存储在内存中。退出进程后,二进制代码存储在硬盘上。二进制占用的空间巨大,一个 js 源码文件大小 1M, 二进制代码需要十几 M

  • 代码复杂度太高

不同的 CPU 对应的指令集是完全不同的。需要针对不同的 CPU 编写代码。

  1. 现有架构
  • 引入字节码

源代码 => AST => 字节码 => 二进制代码 标记重复执行的代码,通过 Turbofan 引擎编译生成效率更高的二进制代码。再次运行时直接执行高效代码。

  • 好处

启动时间较快,启动只需要编译出字节码。执行的时候逐句执行字节码,编译字节码的速度远快于便于出二进制代码的速度。

内存占用变小。字节码占用空间远小于二进制代码占用空间。

解决了代码复杂度的问题。

内存结构

  1. 新生代内存区(new space)

新生代内存区会被划分为两个 semispace,每个 semispace 大小默认为 16MB 也就是说新生代内存区通常只有 32MB 大小(64 位),而这两个 semispace 分别是 from space 和 to space(具体有什么用下文会说),通常新创建的对象会先放入这两个 semispace 中的一个。

  1. 老生代内存区(old space)

通常会较为持久的保存对象,也分为两个区域 old pointer space 和 old data space (各 700MB)分别用来存放 GC 后还存活的指针信息和数据信息。

  1. 大对象区(large object space)

这里存放体积超越其他区大小的对象,主要为了避免大对象的拷贝,使用该空间专门存储大对象。

  1. 单元区、属性单元区、Map 区(Cell space、property cell space、map space)

Map 空间存放对象的 Map 信息也就是隐藏类(Hiden Class)最大限制为 8MB;每个 Map 对象固定大小,为了快速定位,所以将该空间单独出来。

  1. 代码区 (code Space)

主要存放代码对象,最大限制为 512MB,也是唯一拥有执行权限的内存

垃圾回收

v8 引擎垃圾回收主要是针对堆内存的。这是因为栈内存存储的基础类型大小是固定的,操作系统能够自动分配和释放回收。

分代回收

垃圾回收主要对两块区域进行回收:新生代和老生代。

  1. 新生代

新生代分为两块,from 和 to。各为 32M 大小。通过复制转移算法清理垃圾。

当 from 中的内存空间快满的时候,则会将 from 中不可达的对象标记起来。然后把未标记的复制到 to 中。清除 from 中的内容。然后将 from 和 to 进行角色互换。

  1. 老生代

当一个对象进行过复制转移算法,并且大小大于 to 空间的 25% 的时候,会被晋升到老生代。

老生代使用标记清除和标记整理。

标记:对老生代对象进行扫描,标记活动的对象。

清除:清除未标记的对象。

整理:清除对象后,会产生大量的内存碎片。如果需要给某个对象分配大内存,可能放不下。因此需要整理。将剩下的活动对象整理到内存的一侧,这样就去除了碎片。

全停顿

js 代码运行要用到 js 引擎,垃圾回收也要用到 js 引擎,如果发生冲突,怎么办?

js 引擎与垃圾回收冲突时,会优先垃圾回收,垃圾回收完毕后再执行 js 代码。这个过程,称为 全停顿

垃圾回收优化

针对全停顿的问题,v8 提出了 Orinoco 优化

  1. 增量标记

因为需要先标记后清除。增量标记就是对标记做优化。

当垃圾达到一定数量时,标记时间比较长。

增量标记就是标记一点,继续代码运行。然后再标记一点,再代码运行。就这样分段进行标记。

最常用的就是三色标记法。

  1. 惰性清理

就算是不清理垃圾,剩余的空间也足以让 JS 代码跑起来。这时就会进行延迟清理。先让 js 代码执行,或者只清理部分垃圾,而不清理全部。这就是惰性清理。

  1. 并行

并行式 GC 允许主线程和辅助线程同时执行同样的 GC 工作,这样可以让辅助线程来分担主线程的 GC 工作,使得垃圾回收所耗费的时间等于总时间除以参与的线程数量(加上一些同步开销)。

比如:新生代复制的过程就是并行

  1. 并发

V8 在老生代垃圾回收中,如果堆中的内存大小超过某个阈值之后,会启用并发(Concurrent)标记任务。每个辅助线程都会去追踪每个标记到的对象的指针以及对这个对象的引用,而在 JavaScript 代码执行时候,并发标记也在后台的辅助进程中进行,当堆中的某个对象指针被 JavaScript 代码修改的时候,写入屏障(write barriers)技术会在辅助线程在进行并发标记的时候进行追踪。

写屏障

在一次完成 GC 标记暂停中,如果执行任务程序时内存中存在的变量引用关系被改变了,这样会导致此次 GC 存在问题。所以 V8 将 变量引用关系记录起来了。