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 引擎的执行流程:
- Parser 将源码解析为 AST(支持懒解析优化首次加载)
- Ignition 将 AST 编译为字节码并执行,同时收集类型反馈
- TurboFan 对热点代码进行优化编译,生成高效机器码
- GC 自动管理内存,分代回收 + 增量标记保证低延迟
理解 V8 的工作原理,能帮助你写出更符合引擎优化路径的代码,从根本上提升 JavaScript 的运行性能。