谈一谈V8引擎的运行原理

197 阅读7分钟

前言

上一篇文章我们提到了浏览器的渲染过程,HTML、CSS以及JS的解析,其中JS解析依赖于浏览器的JS引擎。因为JavaScript实际是一种高级语言,这种语言并不是机器语言。但是,计算机的CPU只能理解特定的机器语言,它不理解JavaScript语言,所以在计算机上执行JavaScript代码之前,必须将其转换为机器语言

简单来说JavaScript引擎是一个解释和执行JavaScript代码的程序,负责将JavaScript代码转换成可执行的机器代码。 每个浏览器的JS引擎都是不同的,此篇文章我们着重讲一下谷歌浏览器的V8引擎。

V8引擎定义及特性

  • V8 是 Google 开发的一个高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写的开源项目。它用于 Chrome 浏览器、Node.js 等平台。

  • 兼容性:V8 支持 ECMAScriptWebAssembly,并能够在多种操作系统上运行,如 Windows、macOS、和基于 x64、IA-32、ARM 或 MIPS 处理器的 Linux 系统。

  • V8 可以独立运行(虽然很少这么做),更常见的是它被嵌入到 C++ 应用程序中(例如 Node.js)。

  • 主要目标:V8 的主要目标是提高 JavaScript 的性能和执行速度。

V8处理流程

  1. 源代码输入 。浏览器内核接收到js源代码后会将其传递给js引擎,在这也就是v8引擎。

  2. 解析 。通过Scanner(扫描器)进行词法分析Scanner转化结果就是一系列的词法单元(tokens),如关键字、标识符、操作符、字面量等。接下来,这些词法单元会被组织成抽象语法树(AST),该树表示代码的结构和逻辑(变量提升就是在此阶段处理的)。

  3. 初始编译。V8 的初始阶段使用名为 Ignition 的解释器对生成的 AST 进行字节码编译。字节码是一种中间表示,介于源代码和机器码之间,它是跨平台的,并不直接依赖于具体的某一硬件。V8会直接解释执行这些字节码。

  4. 性能监控与优化。在字节码执行期间,V8会持续监控代码的执行情况,特别是那些频繁执行的代码。通过收集类型信息和性能数据,V8 能够识别哪些代码段可以进行优化。这一阶段的优化策略包括:

    • 内联缓存(Inline Caches) :V8 会记录函数调用的类型信息,将频繁调用的小函数直接嵌入到调用方,减少函数调用的开销。

    • 优化编译器(TurboFan) :V8 中有一个 TurboFan 优化编译器,它监控运行时性能,并根据收集到的类型信息,对频繁执行的代码进行优化,将其编译为高效的机器码。

  5. 优化代码生成。一旦 V8 检测到某些函数或代码段执行频繁,TurboFan 会将这些代码段编译为更高效的机器码,替代之前的字节码执行,进一步提升执行效率

  6. 回退。在某些情况下,如果代码的执行环境发生变化,例如变量类型发生变化不再符合优化编译时的假设,V8 可能会将优化的机器码回退为字节码执行,这种情况被称为 “反优化(Deoptimization)”。这种机制解释了为什么在 TypeScript 项目中,确定的类型信息可以减少反优化的概率,从而提高执行效率。

  7. 执行。V8 解释并执行字节码/机器码,最终完成js代码的运行

词法分析

这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元/token(词法单元是组成代码的最小单位,比如关键字、标识符、运算符、数字、字符串等)。

词法分析过程

  • 输入: 接收整个源代码的文本。

  • 分解: 将源代码按照字符顺序通过Scanner进行逐步扫描,并将其分解为一个个词法单元/token

  • 输出: 输出这些词法单元作为后续语法分析的输入

例如,程序let a = 2;可以分解为以下token:

[    { "type": "Keyword", "value": "let" },     { "type": "Identifier", "value": "x" },     { "type": "Operator", "value": "=" },     { "type": "Literal", "value": "10" },     { "type": "Punctuation", "value": ";" } ]
  • type 表示该词法单元的类型,例如 KeywordIdentifierLiteral 等。

  • value 表示该词法单元在代码中实际的文本值,例如 "let""x""=" 等。

在大多数情况下,空白字符(如空格、换行符、制表符)不会被转换为独立的词法单元,除非它们在特定上下文中有语义意义(例如模板字符串中的空格)此外,除了 typevalue ,在实际使用中,词法单元可能包含更多的字段,如startend记录词法单元在源代码中的开始和结束位置;linecolumn字段记录词法单元在源代码中的行号和列号等。

语法分析

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为 “抽象语法树”(Abstract Syntax Tree,AST)

语法分析过程

  • 输入: 接收由词法分析器生成的一系列词法单元(Tokens)。

  • 预解析(Pre-parsing): 这是语法分析的一个优化阶段,并不是所有的js代码在一开始都会被执行,那么如果对所有的js代码进行解析,必然会影响网页的运行效率。预解析会跳过暂时不需要解析的代码,以减少加载开销。

  • 解析(Parsing): 根据 JavaScript 的语法规则,生成AST.

  • 输出: 生成的AST将作为后续编译、优化或解释执行的基础。

例如代码let a = 2;的AST表示如下:

{
    "type": "VariableDeclaration", 
    "kind": "let", 
    "declarations": [
        { 
            "type": "VariableDeclarator", 
            "id": { 
                "type": "Identifier", 
                "name": "a" 
            }, 
            "init": { 
                "type": "Literal", // 或者 "NumericLiteral" 取决于解析器 
                "value": 2, 
                "raw": "2" 
            } 
        } 
    ] 
}

大家也可以使用AST Explorer工具来生成和查看js代码的AST。

V8的核心组件

Ignitio解释器

负责将 JavaScript 源代码经过解析生成的 AST 转换为字节码,并快速启动代码的执行

TurboFan优化编译器

它基于收集到的运行时信息,针对性能关键的代码路径进行优化编译,将字节码编译为高效的机器码。

我们上面提到计算机CPU 只能理解特定的机器语言,不能直接理解其他语言。机器码是直接由CPU执行的代码,可以直接在硬件上运行并没有额外的解释开销,而字节码是一种中间表示形式,需要由解释器(如 V8 的 Ignition)逐条解释和执行,这些额外的解释开销会降低代码的执行速度。另外机器码可以进行深度的编译器优化,针对特定硬件架构生成高效代码。

所以机器码效率要比字节码高。

Inline Caches内联缓存

通过记录函数调用的类型信息,内联缓存优化了函数调用,跳过了类型检查,加速了后续执行。

Garbage Collector垃圾回收

V8 引擎内置了高效的垃圾回收器(GC),自动管理内存分配和回收,避免内存泄漏。之后会开篇文章单独讲垃圾回收机制,在此不做过多讲解

好了以上就是此篇文章的相关内容了,大家如果觉得有所帮助可以给个赞。有不同观点也欢迎评论指出

撒花🎉🎉🎉