【重构基础】V8引擎

131 阅读6分钟

什么是V8引擎

V8引擎是谷歌使用C++编写的一个开源的高性能JavaScript和WASM引擎。前端开发常用的Chrome内核就是使用V8引擎来执行JS代码。Node.js也是采用V8引擎来执行。

JIT(Just-In-Time Compilation)

中文全称为即时编译,是一种用来提升程序的执行的编译技术。它集合了静态执行和解释执行两种传统方法的优点,做到了能够快速执行的同时,利用运行时的信息来对代码进行优化,提升了执行时的效率。

提提什么是静态执行和解释执行

  • 静态执行,在程序运行之前就将源代码完全编译成机器码,这样可以快速执行,但缺乏运行时的灵活性。例如C++是直接将代码转换成机器码、Java是通过JVM将中间码转换成机器码
  • 解释执行,在执行程序时一行行的解释成机器码进行执行。这个运行过程是解释一行,执行一行。例如Python等

来说说V8是怎么执行你的JS代码的

例如说在浏览器的开发者工具的Console中输入以下的代码

(function(){
  console.log('bar');
})()

在V8引擎执行代码的时候,V8引擎首会先去初始化执行JS代码的环境

  • 内存分配:堆(HEAP)和栈(STACK)空间
  • 全局环境设置:全局执行上下文(Context)、全局作用域(Scope)
  • 垃圾回收的初始化:包括标记-清除(Mark-Sweep)、标记-整理(Mark-Compact)以及增量标记(Incremental Marking)等算法
  • 事件循环:V8 初始化必要的事件处理机制和异步调度器。这包括事件循环的设置,以及与宿主环境的接口,如 Web APIs 或 Node.js 的 I/O 操作。
  • 安全性和沙箱机制等其他操作

并且会会执行以下的操作

  1. 解析(Parsing):这一阶段是大学本科的计算机的编译原理,感兴趣可以去学习一下
    1. 词法分析:V8引擎将获取到的所有代码进行拆分,获得一个个词法单元(tokens),并且获取到对应的关键字
    2. 语法分析:将上一步中获取到的词法单元(tokens)通过词法分析器(Lexer)进行分析,最终组合成一个AST抽象语法树,AST抽象语法树是一个深层嵌套的对象结构,用树这种形式来表达整个程序的语法结构。AST转换器
{
  "type": "Program",
  "start": 0,
  "end": 40,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 39,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 39,
        "callee": {
          "type": "FunctionExpression",
          "start": 1,
          "end": 36,
          "id": null,
          "expression": false,
          "generator": false,
          "async": false,
          "params": [],
          "body": {
            "type": "BlockStatement",
            "start": 11,
            "end": 36,
            "body": [
              {
                "type": "ExpressionStatement",
                "start": 15,
                "end": 34,
                "expression": {
                  "type": "CallExpression",
                  "start": 15,
                  "end": 33,
                  "callee": {
                    "type": "MemberExpression",
                    "start": 15,
                    "end": 26,
                    "object": {
                      "type": "Identifier",
                      "start": 15,
                      "end": 22,
                      "name": "console"
                    },
                    "property": {
                      "type": "Identifier",
                      "start": 23,
                      "end": 26,
                      "name": "log"
                    },
                    "computed": false,
                    "optional": false
                  },
                  "arguments": [
                    {
                      "type": "Literal",
                      "start": 27,
                      "end": 32,
                      "value": "bar",
                      "raw": "'bar'"
                    }
                  ],
                  "optional": false
                }
              }
            ]
          }
        },
        "arguments": [],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}
  1. 编译成字节码(Ignition):V8引擎会通过Ignition将解析(Parsing)得到的AST语法树进行字节码的转换,这些字节码就会表示这段IIFE程序的创建和调用

  2. 优化编译(TurboFan):在V8执行到性能消耗比较大的代码的时候会触发JIT中的优化编译,在运行到触发频率比较高或者操作比较复杂的代码的时候,V8很有可能会将它标记为热点,并将其送到TurboFan中,并且将运行时收集到的信息(变量类型,执行路径等)进行高级优化,生成更加高效的机器码,去对原字节码进行替换,提升执行效率。但其实在有些时候,优化并不一定是一个正向的优化,例如说上面的过程中会给代码加上一个性能监控,并且在上面TurboFan的优化之后生成的代码也有可能会出现假设错误,如果类型假设错误,V8会重新执行优化。在优化的过程中,假如说优化过后的代码会相较于之前的版本有更大的性能消耗,V8会执行字节码的回退

  3. 执行和输出:执行上述操作之后获得的机器码会被Ignition解释器运行在CPU上

  4. 垃圾回收:在执行完毕之后,V8引擎会执行GC,将已经无法被V8访问到的局部变量和临时对象进行标记清除的操作

为什么Ignition采用的是字节码而不是机器码

首先,了解字节码和机器码之间的基本区别是非常重要的:

  • 字节码 是一种中间表示(Intermediate Representation, IR),它比源代码更低级,但不是特定于任何一个处理器架构的机器码。字节码是为了便于快速解释执行而设计的,通常由虚拟机执行,可以在不同平台上运行,只要有相应的虚拟机实现。
  • 机器码 是直接由计算机硬件执行的低级代码,特定于特定的处理器架构。它是最终的执行格式,速度最快,但缺乏灵活性,因为它是针对特定硬件设计的。

读完上面我想大家应该都懂了吧,V8其实是一个虚拟机,它可以在不同平台的场景下使用,JavaScript不像C语言需要对对应的平台将自己的代码转换成对应的机器码才能执行。

并且采用字节码相对于机器码有下面这些好处

  1. 跨平台兼容性:字节码可以在任何支持相应虚拟机的平台上运行,不受具体硬件架构的限制。这使得 V8 可以在多种硬件和操作系统上运行而无需修改。

  2. 启动性能:将 JavaScript 代码编译成字节码通常比编译成机器码要快。这对于动态或交互式环境(如网页)中的脚本来说尤其重要,因为这些环境下快速解释执行代码比等待更长时间的编译过程更为关键。

  3. 内存使用:相较于机器码,字节码通常更加紧凑,占用较少的内存。在移动设备和资源受限的环境中,这一点尤其重要。

  4. 优化策略:使用字节码允许 V8 在不牺牲初次执行速度的情况下,收集关于代码如何运行的信息(即所谓的“热点”代码)。这些信息随后可以用来指导更高级的 JIT 编译器(如 TurboFan)进行更为精细的优化和编译成机器码,这种方式称为“分层编译”。

  5. 灵活性与维护性:字节码更易于生成和操作,为开发和维护虚拟机提供了更多的灵活性。它也使得实现高级语言特性和进行实验性改进更为容易。