2025年09月10日11 分钟

V8 引擎深度解析:JavaScript 是如何被执行的

深入 Chrome V8 引擎的工作原理,从源码到机器码的完整流程,涵盖解析、编译、垃圾回收等核心机制

什么是 V8 引擎

V8 是 Google 开发的开源高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写。它被用在 Chrome 浏览器和 Node.js 中。

V8 的核心目标只有一个:把 JavaScript 源码尽可能快地变成机器码执行

你写的 JS 代码 → V8 引擎 → 机器码 → CPU 执行

V8 的整体架构

                    JavaScript 源码
                         │
                    ┌────▼────┐
                    │  Parser  │  词法分析 + 语法分析
                    └────┬────┘
                         │
                      AST(抽象语法树)
                         │
                    ┌────▼────┐
                    │ Ignition │  解释器 → 生成字节码
                    └────┬────┘
                         │
                    Bytecode(字节码)
                         │
              ┌──────────┼──────────┐
              │    收集类型反馈信息    │
              │                      │
         冷代码(直接执行)        热代码(频繁执行)
              │                      │
              │               ┌──────▼──────┐
              │               │  TurboFan    │  优化编译器
              │               └──────┬──────┘
              │                      │
              │              Optimized Machine Code
              │                      │
              └──────────┬───────────┘
                         │
                    CPU 执行结果

第一步:解析(Parsing)

V8 拿到 JS 源码后的第一步是解析,分为两个阶段:

1.1 词法分析(Lexical Analysis / Tokenization)

将源码字符串拆分成有意义的词法单元(Token)

// 源码
const sum = a + b;

// 词法分析后的 Token 流
[
  { type: 'Keyword',    value: 'const' },
  { type: 'Identifier', value: 'sum' },
  { type: 'Punctuator', value: '=' },
  { type: 'Identifier', value: 'a' },
  { type: 'Punctuator', value: '+' },
  { type: 'Identifier', value: 'b' },
  { type: 'Punctuator', value: ';' },
]

1.2 语法分析(Syntax Analysis)

将 Token 流转换成抽象语法树(AST)

{
  "type": "VariableDeclaration",
  "kind": "const",
  "declarations": [{
    "id": { "type": "Identifier", "name": "sum" },
    "init": {
      "type": "BinaryExpression",
      "operator": "+",
      "left":  { "type": "Identifier", "name": "a" },
      "right": { "type": "Identifier", "name": "b" }
    }
  }]
}

你可以在 AST Explorer 上实时查看任何 JS 代码的 AST 结构。

1.3 懒解析(Lazy Parsing)

V8 有一个重要优化:不是所有代码都会被立即完整解析

function heavyFunction() {
  // 这个函数体很大,但可能永远不会被调用
  // V8 只会做"预解析"(Pre-parsing),记录函数边界
  // 只有真正调用时才完整解析
}

heavyFunction() // 此时才触发完整解析

预解析只验证语法合法性和变量作用域,不生成 AST,节省了首次加载时间

第二步:Ignition 解释器

AST 生成后,交给 Ignition(点火器)解释器,逐条翻译成字节码(Bytecode)

为什么不直接编译成机器码?

方案优点缺点
直接编译成机器码执行最快编译慢,内存占用大
只用解释器执行启动快,内存小执行慢
字节码 + JIT 优化(V8 的方案)平衡启动速度和执行速度实现复杂

字节码示例:

// 源码
function add(a, b) {
  return a + b;
}

// Ignition 生成的字节码(简化)
// Ldar a1         // 加载参数 a 到累加器
// Add a2          // 累加器 += 参数 b
// Return          // 返回累加器的值

字节码比源码紧凑,比机器码轻量,是一种中间表示

类型反馈(Type Feedback)

Ignition 在执行字节码时,会收集运行时信息,这些信息称为"类型反馈":

function add(a, b) {
  return a + b
}

add(1, 2)       // Ignition 记录:a 是 number,b 是 number
add(3, 4)       // 再次确认:都是 number
add(5, 6)       // 三次都是 number → 标记为"热点函数"

当某个函数被调用足够多次(通常几百次到上千次),V8 就会把它交给 TurboFan 优化编译。

第三步:TurboFan 优化编译器

TurboFan 是 V8 的优化编译器,它根据 Ignition 收集的类型反馈,生成高度优化的机器码

优化策略

内联(Inlining):把小函数直接"展开"到调用处

// 优化前
function square(x) { return x * x }
function calc(a) { return square(a) + 1 }

// TurboFan 内联后(概念上)
function calc(a) { return a * a + 1 }  // 省去了函数调用开销

隐藏类(Hidden Classes):V8 为动态对象创建内部结构描述

// V8 内部为这种固定结构的对象创建 Hidden Class
// 使得属性访问可以用固定偏移量,接近 C++ 结构体的速度
const point = { x: 1, y: 2 }
point.x  // 不需要字典查找,直接通过偏移量访问

类型特化(Type Specialization)

// Ignition 收集到的信息:a 和 b 一直是 number
function add(a, b) { return a + b }

// TurboFan 生成的优化机器码(伪代码):
// 直接使用 CPU 的整数加法指令
// 不需要检查类型、不需要处理字符串拼接
// MOV EAX, [a]
// ADD EAX, [b]
// RET

去优化(Deoptimization)

如果运行时假设被打破,TurboFan 会回退到字节码执行

function add(a, b) { return a + b }

// 前 1000 次调用都是 number,TurboFan 生成了优化的整数加法机器码
add(1, 2)
add(3, 4)
// ...

// 突然传入字符串!
add("hello", "world")
// 😱 去优化触发!
// TurboFan 的机器码被丢弃,回退到 Ignition 字节码执行

这就是为什么保持参数类型稳定对 V8 性能至关重要。

第四步:垃圾回收(Garbage Collection)

V8 使用分代垃圾回收策略:

         V8 堆内存
  ┌─────────┬────────────────┐
  │  新生代   │     老生代       │
  │(Young Gen)│  (Old Gen)     │
  │  1~8MB   │   几百MB~几GB   │
  ├─────────┤                │
  │From│ To  │                │
  │空间│ 空间 │                │
  └────┴────┴────────────────┘

新生代:Scavenge 算法

1. 新对象分配在 From 空间
2. From 满了 → 触发 GC
3. 遍历 From,把"活的"对象复制到 To 空间
4. 清空 From
5. From 和 To 互换角色

From: [A][B][C][D]    → 检查引用 → A、C 还活着
  To: [ ][ ][ ][ ]

From: [ ][ ][ ][ ]    ← 清空
  To: [A][C][ ][ ]    ← 存活对象复制过来

如果一个对象经历了两次 Scavenge 仍然存活 → 晋升到老生代

老生代:Mark-Sweep + Mark-Compact

Mark 阶段:从根对象(全局对象、调用栈)出发,标记所有可达对象
Sweep 阶段:回收未标记的对象
Compact 阶段:整理内存碎片,把存活对象移到一端

 标记前: [A][ ][B][ ][ ][C][ ][D]
 标记后: [A*][ ][B*][ ][ ][C][ ][D*]  (* = 可达)
 清除后: [A*][ ][B*][ ][ ][ ][ ][D*]  (C 被回收)
 整理后: [A*][B*][D*][ ][ ][ ][ ][ ]  (紧凑排列)

增量标记(Incremental Marking)

老生代 GC 可能耗时很长,V8 使用增量标记来避免长时间停顿:

传统 GC:  JS执行 ──暂停──[======GC======]──恢复── JS执行
                          200ms 停顿 😱

增量 GC:  JS执行 ─[GC]─ JS ─[GC]─ JS ─[GC]─ JS执行
                   5ms    5ms    5ms
                   多次短暂停顿,用户无感知 ✅

实际开发中的性能启示

1. 保持函数参数类型稳定

// ❌ 类型不稳定 → 无法优化
function process(input) {
  return input.toString()
}
process(123)
process("abc")
process(true)

// ✅ 类型稳定 → TurboFan 可以优化
function processNumber(n: number) {
  return n.toString()
}

2. 避免动态添加/删除对象属性

// ❌ 破坏 Hidden Class
const obj = { x: 1, y: 2 }
delete obj.x            // Hidden Class 失效
obj.z = 3               // 创建新的 Hidden Class

// ✅ 初始化时确定所有属性
const obj = { x: 1, y: 2, z: 0 }
obj.z = 3               // 属性已存在,Hidden Class 不变

3. 注意闭包导致的内存泄漏

// ❌ 闭包持有大数组引用,无法被 GC
function createHandler() {
  const hugeData = new Array(1000000).fill('x')
  return function handler() {
    console.log(hugeData.length) // hugeData 被闭包捕获,无法回收
  }
}

// ✅ 只保留需要的数据
function createHandler() {
  const hugeData = new Array(1000000).fill('x')
  const length = hugeData.length  // 只保留需要的值
  return function handler() {
    console.log(length) // hugeData 可以被 GC
  }
}

4. 以相同顺序初始化对象属性

// ❌ 不同顺序 → 不同的 Hidden Class → 无法共享优化
const a = { x: 1, y: 2 }
const b = { y: 2, x: 1 }  // 和 a 的 Hidden Class 不同!

// ✅ 相同顺序 → 共享 Hidden Class
const a = { x: 1, y: 2 }
const b = { x: 3, y: 4 }  // 共享同一个 Hidden Class ✅

总结

V8 引擎的执行流程:

  1. Parser 将源码解析为 AST(支持懒解析优化首次加载)
  2. Ignition 将 AST 编译为字节码并执行,同时收集类型反馈
  3. TurboFan 对热点代码进行优化编译,生成高效机器码
  4. GC 自动管理内存,分代回收 + 增量标记保证低延迟

理解 V8 的工作原理,能帮助你写出更符合引擎优化路径的代码,从根本上提升 JavaScript 的运行性能。