Skip to main content

V8(JS 引擎)研究笔记

V8 是一个由 Google 开发的开源 JavaScript 引擎,用于 Google Chrome 及 Chromium 中。V8 可以独立运行,也可以嵌入到任何 C++应用程序中。其命名灵感源自 50 年代经典的“肌肉车”的引擎, 是一种具有 8 个气缸程 V 型排列的汽车发动机。


运作方式#

V8 在运行之前将 JavaScript 编译成了机器码,而非字节码或是解释执行它,以此提升性能。更进一步,使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript 程序与 V8 引擎的速度媲美二进制编译。

传统的 Javascript 是动态语言,又可称之为 Prototype-based Language,JavaScript 继承方法是使用 prototype,透过指定 prototype 属性,便可以指定要继承的目标。属性可以在运行时添加到或从对象中删除,引擎会为运行中的对象创建一个属性字典,新的属性都要透过字典查找属性在内存中的位置。V8 为 object 新增属性的时候,就以上次的 hidden class 为父类别,创建新属性的 hidden class 的子类别,如此一来属性访问不再需要动态字典查找了。

为了缩短由垃圾回收造成的停顿,V8 使用 stop-the-world, generational, accurate 的垃圾回收器。在执行回收之时会暂时中断程序的执行,而且只处理对象堆栈。还会收集内存内所有对象的指针,可以避免内存溢出的情况。V8 汇编器是基于 Strongtalk 汇编器。

静态分析器 (Parser)#

负责将 JS 源码(字符串)解析为抽象语法树 (AST), 是源代码语法结构的一种抽象表示。为语法解析做准备。

立即解析与懒解析#

立即解析需要立即编译的函数。它主要做三件事:构建 AST,构建作用域层级和查找所有语法错误。另一方面, 懒解析只运行未编译的函数。它不构建 AST,也不查找所有语法错误,它只构建作用域层级,与立即解析相比节省了大约一半的时间。

解释器 (Ignition)#

负责将 AST 转换为字节码,解释执行字节码,同时收集编译器优化编译所需的信息,比如函数参数的类型。

编译器 (TurboFan)#

利用解释器所收集的类型信息,将字节码转换为优化的汇编代码。

  • 如果函数没有被调用,则 V8 不会去编译它。
  • 如果函数只被调用 1 次,则解析器将其编译字节码就直接解释执行了。编译器不会进行优化编译,因为它需要解析器收集函数执行时的类型信息。这就要求函数至少需要执行 1 次,编译器才有可能进行优化编译。
  • 如果函数被调用多次,则它有可能会被识别为热点函数,且解析器收集的类型信息证明可以进行优化编译的话,这时编译器则会将字节码编译为机器码(Optimized Machine Code),以提高代码的执行性能。

简单地说,解析器将 JS 源码转换为 AST,然后解释器将 AST 转换为字节码,最后编译器将字节码转换为经过优化的机器码。

垃圾回收 (Orinoco)#

V8 引擎会自动分配并回收内存,不需要我们手动管理,这也意味着程序员将无法掌控内存。ECMAScript 没有暴露任何垃圾回收器的接口。我们无法强迫其进行垃圾回收,更无法干预内存管理。

标记指针法:这种方法需要在每个字末位预留一位来标记这个字段是指针还是数据。这种方法需要编译器支持,但实现简单,而且性能不错。V8 采用的是这种方式。V8 将所有数据以 32bit 字宽来存储,其中最低一位保持为 0,而指针的最低两位为 01

V8 主要将内存分为新生代和老生代两代,新生代中的对象存活时间较短,老生代中的对象存活时间较长或是常驻内存的对象

V8 堆的整体大小就是新生代内存加老生代内存,--max-old-space-size 设置的就是老生代内存,--max-new-space-size 设置的就是新生代的内存,老生代内存在 64 位系统下是 1400MB,在 32 位系统下是 700MB。这也是 V8 引擎的最大使用内存限制,这是为了单次垃圾回收时不至于耗时太长。

// TODO 垃圾回收机制如何判断对象是否存活。

特性#

  • V8 实现脚本流(script streaming)和代码缓存技术。脚本流即脚本一旦开始下载,async 和 deferred 的 脚本就会在单独的线程上解析。这意味着在下载脚本完成后几乎立即完成解析,这会提升 10% 的页面加载速度。
  • 每次访问页面时,JavaScript 代码通常编译为字节码。 然而,一旦用户访问另一页面,该字节码就被丢弃。 发生这种情况是因为编译后的代码很大程度上依赖于编译时机器的状态和上下文。 这是 Chrome 42 引入字节码缓存的原因。 该技术会本地缓存编译过的代码,这·回同一页面时,诸如下载,解析和编译等所有步骤都会被跳过。 这使得 Chrome 可以节省大约 40% 的解析和编译时间。 此外,这还可以节省移动设备的电量。

注释#

字节码#

  • 字节码(英语:Bytecode)通常指的是已经经过编译,但与特定机器代码无关,需要解释器转译后才能成为机器代码的中间代码。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。
  • 字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。字节码的实现方式是通过编译器和虚拟机。编译器将源码编译成字节码,特定平台上的虚拟机将字节码转译为可以直接运行的指令。字节码的典型应用为 Java bytecode。

机器码#

  • 机器语言(machine language)是一种指令集的体系。这种指令集称为机器代码(machine code),是电脑的 CPU 可直接解读的资料。
  • 机器代码有时也被称为原生码(Native Code),这个名词比较强调某种编程语言或库与运行平台相关的部分。

参考#