1035 字
3 分钟
JavaScript 引擎原理:V8 执行流程
前言
JavaScript 是一门解释型语言,但现代 JavaScript 引擎(如 V8)通过 JIT(Just-In-Time)编译技术,使其性能接近编译型语言。本文深入剖析 V8 引擎的执行流程,帮助你理解 JavaScript 代码是如何被高效执行的。
V8 架构概览
flowchart TB
subgraph 源码处理
A[JavaScript 源码] --> B[解析器 Parser]
B --> C[抽象语法树 AST]
end
subgraph 字节码生成
C --> D[字节码生成器]
D --> E[字节码 Bytecode]
end
subgraph 执行引擎
E --> F[解释器 Ignition]
F --> G[执行字节码]
G --> H[收集类型反馈]
end
subgraph 优化编译
H --> I[编译器 TurboFan]
I --> J[优化机器码]
J --> K[执行优化代码]
K --> |去优化| F
end
一、解析阶段(Parsing)
1.1 词法分析(Lexical Analysis)
将源码字符串转换为 Token 流:
// 源码const x = 1 + 2;
// 词法分析后的 Token 流[ { type: "Keyword", value: "const" }, { type: "Identifier", value: "x" }, { type: "Punctuator", value: "=" }, { type: "Numeric", value: "1" }, { type: "Punctuator", value: "+" }, { type: "Numeric", value: "2" }, { type: "Punctuator", value: ";" },];词法分析器状态机:
stateDiagram-v2
[*] --> Start
Start --> Identifier: 字母/_
Start --> Number: 数字
Start --> String: 引号
Start --> Operator: 运算符
Start --> Whitespace: 空白
Identifier --> Identifier: 字母/数字/_
Identifier --> [*]: 其他
Number --> Number: 数字/点
Number --> [*]: 其他
String --> String: 非引号
String --> [*]: 引号
Operator --> [*]
Whitespace --> Start
1.2 语法分析(Syntax Analysis)
将 Token 流转换为抽象语法树(AST):
// 源码function add(a, b) { return a + b;}
// AST 结构(简化){ type: 'FunctionDeclaration', id: { type: 'Identifier', name: 'add' }, params: [ { type: 'Identifier', name: 'a' }, { type: 'Identifier', name: 'b' } ], body: { type: 'BlockStatement', body: [{ type: 'ReturnStatement', argument: { type: 'BinaryExpression', operator: '+', left: { type: 'Identifier', name: 'a' }, right: { type: 'Identifier', name: 'b' } } }] }}V8 解析器特点:
| 特性 | 说明 |
|---|---|
| Pre-parser | 预解析器,延迟解析非立即执行代码 |
| Lazy parsing | 懒解析,函数首次调用时才完整解析 |
| Scope analysis | 作用域分析,确定变量引用关系 |
1.3 懒解析优化
// 外层代码立即解析function outer() { // inner 函数体延迟解析(首次调用时才解析) function inner() { console.log("lazy parsed"); }
// 调用时才触发完整解析 inner();}sequenceDiagram
participant P as Parser
participant PP as Pre-parser
participant S as Source
S->>P: 解析 outer 函数声明
P->>PP: 遇到 inner,交给预解析器
PP-->>P: 返回函数边界信息
Note over P: 继续解析后续代码
P->>P: 调用 inner 时完整解析
二、字节码生成
2.1 字节码介绍
字节码是介于源码和机器码之间的中间表示:
┌─────────────────────────────────────────┐│ JavaScript 源码 ││ const x = 1 + 2; │└─────────────────┬───────────────────────┘ ↓┌─────────────────────────────────────────┐│ 字节码(简化表示) ││ Ldar a0 // 加载参数 a ││ Add a1, [0] // 加上参数 b ││ Return // 返回结果 │└─────────────────┬───────────────────────┘ ↓┌─────────────────────────────────────────┐│ 机器码(x64) ││ mov rax, [rbp-0x10] ││ add rax, [rbp-0x18] ││ ret │└─────────────────────────────────────────┘2.2 字节码指令集
V8 字节码指令集(部分):
| 指令 | 操作 |
|---|---|
Ldar | Load accumulator from register |
Star | Store accumulator to register |
Add | 加法运算 |
Sub | 减法运算 |
Mul | 乘法运算 |
LdaGlobal | 加载全局变量 |
StaGlobal | 存储全局变量 |
Call | 函数调用 |
Return | 返回 |
Jump | 跳转 |
TestEqual | 相等比较 |
2.3 字节码生成示例
// 源码function sum(a, b) { return a + b;}
// 生成的字节码(简化)// 参数 a 在寄存器 a0, b 在寄存器 a1Ldar a0 // 加载 a 到累加器Add a1, [0] // 累加器 + b,结果存入累加器Return // 返回累加器值// 源码:条件语句function abs(x) { if (x >= 0) { return x; } return -x;}
// 字节码(简化)Ldar a0 // 加载 xStar r0 // 存入 r0LdaSmi [0] // 加载立即数 0TestGreaterThanOrEqual r0 // 比较 x >= 0JumpIfFalse [10] // 条件为假跳转到地址 10Ldar a0 // 返回 xReturnLdar a0 // 加载 xNeg // 取负Return三、解释执行(Ignition)
3.1 Ignition 解释器
Ignition 是 V8 的字节码解释器:
flowchart TB
A[字节码] --> B[取指令 Fetch]
B --> C[解码 Decode]
C --> D[执行 Execute]
D --> E{下一条指令}
E -->|跳转| B
E -->|返回| F[结果]
D --> G[收集类型反馈]
G --> H[反馈向量]
3.2 类型反馈收集
解释器在执行时收集运行时类型信息:
function add(a, b) { return a + b;}
// 第一次调用:整数add(1, 2); // 记录:a 是 Smi,b 是 Smi,结果是 Smi
// 第二次调用:还是整数add(3, 4); // 确认:参数类型稳定
// 第三次调用:字符串add("a", "b"); // 类型变化!触发去优化反馈向量结构:
FeedbackVector for add:┌────────────────────────────────────────┐│ Slot 0: BinaryOp (+) ││ ├── 类型: Smi + Smi → Smi ││ └── 调用次数: 2 │├────────────────────────────────────────┤│ Slot 1: CallCount ││ └── 调用次数: 3 │└────────────────────────────────────────┘3.3 内联缓存(Inline Cache)
V8 使用内联缓存加速属性访问:
const obj = { x: 1, y: 2 };
function getX(o) { return o.x; // 第一次:查找属性位置 // 后续:直接使用缓存的位置}
getX(obj); // 缓存:obj 的 x 在 offset 0getX(obj); // 命中缓存,直接访问flowchart LR
A[访问 o.x] --> B{IC 缓存?}
B -->|命中| C[直接访问 offset]
B -->|未命中| D[查找属性]
D --> E[缓存结果]
E --> F[返回值]
C --> F
四、优化编译(TurboFan)
4.1 TurboFan 编译器
TurboFan 是 V8 的优化编译器:
flowchart TB
A[字节码 + 类型反馈] --> B[构建图]
B --> C[优化阶段]
C --> D[降低阶段]
D --> E[代码生成]
E --> F[优化机器码]
subgraph 优化阶段
C1[内联]
C2[逃逸分析]
C3[循环优化]
C4[死代码消除]
end
C --> C1 --> C2 --> C3 --> C4
4.2 内联优化
将函数调用替换为函数体:
// 原始代码function add(a, b) { return a + b;}function compute(x) { return add(x, 10);}
// 内联后function compute(x) { return x + 10; // add 函数被内联}内联决策因素:
| 因素 | 影响 |
|---|---|
| 函数大小 | 小函数更可能内联 |
| 调用频率 | 热点函数更可能内联 |
| 类型稳定性 | 类型稳定的更可能内联 |
| 调用深度 | 避免过度内联 |
4.3 逃逸分析
分析对象是否”逃逸”出函数:
function createPoint(x, y) { return { x, y }; // 对象逃逸(返回给外部)}
function compute(x, y) { const point = { x, y }; // 对象未逃逸 return point.x + point.y;}
// 逃逸分析后,可以标量替换function compute(x, y) { // const point = { x, y }; // 消除对象分配 return x + y; // 直接使用标量}4.4 Sea of Nodes
TurboFan 使用图表示(Sea of Nodes):
// 源码// return a + b * c;
计算图: Load(a) Load(b) Load(c) | | | | Multiply------+ | | +---Add----+ | Return节点类型:
| 类型 | 说明 |
|---|---|
| ValueNode | 值计算 |
| ControlNode | 控制流 |
| EffectNode | 副作用 |
| MemoryNode | 内存操作 |
4.5 优化假设
TurboFan 基于类型反馈做优化假设:
function process(arr) { let sum = 0; for (let i = 0; i < arr.length; i++) { sum += arr[i]; } return sum;}
// 假设:arr 是连续的整数数组(SMI Array)// 优化:// 1. 内联 length 访问// 2. 消除边界检查(如果已验证)// 3. 使用 SIMD 指令加速五、去优化(Deoptimization)
5.1 触发条件
当优化假设失效时,触发去优化:
flowchart TB
A[执行优化代码] --> B{假设检查}
B -->|通过| C[继续执行]
B -->|失败| D[触发去优化]
D --> E[丢弃优化代码]
E --> F[回到解释器]
F --> G[重新收集反馈]
触发条件:
| 条件 | 示例 |
|---|---|
| 类型变化 | add(1, 2) → add('a', 'b') |
| 原型链变化 | 修改对象原型 |
| 隐藏类变化 | 添加新属性 |
| 数组类型变化 | SMI 数组变为对象数组 |
5.2 去优化代价
// 热点函数function hot(arr) { return arr[0] + arr[1];}
// 大量调用,已优化for (let i = 0; i < 100000; i++) { hot([1, 2]); // 整数数组,优化为 SMI 操作}
// 类型改变,触发去优化hot(["a", "b"]); // 字符串数组!
// 后续调用回到解释器hot([3, 4]); // 不再是优化代码去优化的影响:
- 丢弃已编译的优化代码
- 恢复到解释执行
- 需要重新收集类型反馈
- 可能触发重新优化(或标记为不可优化)
六、内存管理
6.1 堆组织
V8 堆内存分为多个区域:
flowchart TB
subgraph 新生代 NewSpace
NS[From/Semi-Space]
NT[To/Semi-Space]
end
subgraph 老生代 OldSpace
OP[Old Pointers]
OD[Old Data]
end
subgraph 大对象 LargeObjectSpace
LO[大对象]
end
subgraph 代码空间 CodeSpace
CO[代码对象]
end
| 区域 | 大小 | 用途 |
|---|---|---|
| New Space | 1-8MB | 新对象分配 |
| Old Space | 动态 | 长期存活对象 |
| Large Object Space | 动态 | 大于 256KB 的对象 |
| Code Space | 动态 | JIT 生成的代码 |
6.2 垃圾回收
V8 使用分代 GC:
flowchart TB
subgraph 新生代 GC
A[分配] --> B[From 空间]
B --> C{GC 触发}
C -->|Scavenge| D[复制存活对象到 To]
D --> E[交换 From/To]
E --> F{存活多次?}
F -->|是| G[晋升到老生代]
F -->|否| A
end
GC 算法:
| 算法 | 区域 | 特点 |
|---|---|---|
| Scavenge | 新生代 | 复制算法,快速 |
| Mark-Sweep-Compact | 老生代 | 标记清除压缩 |
| Incremental | 老生代 | 增量标记,减少停顿 |
| Parallel | 老生代 | 并行标记 |
| Concurrent | 老生代 | 并发标记 |
6.3 隐藏类(Hidden Class)
V8 使用隐藏类优化对象访问:
// 创建两个"形状"相同的对象const p1 = { x: 1, y: 2 };const p2 = { x: 3, y: 4 };
// 它们共享同一个隐藏类// 隐藏类: { x: offset 0, y: offset 1 }flowchart LR
A[空隐藏类 C0] --> B[添加 x: C1]
B --> C[添加 y: C2]
D[p1] --> C
E[p2] --> C
F[p3: 不同顺序] --> G[不同隐藏类]
// 不同顺序创建不同隐藏类const p1 = { x: 1, y: 2 }; // 隐藏类 Aconst p2 = { y: 1, x: 2 }; // 隐藏类 B(不同!)七、性能优化实践
7.1 保持类型稳定
// 不好:类型不稳定function process(value) { if (typeof value === "number") { return value * 2; } return value + value; // 字符串拼接}process(1); // numberprocess("hello"); // string - 触发去优化!
// 好:类型稳定function processNumber(value) { return value * 2;}function processString(value) { return value + value;}7.2 保持对象形状一致
// 不好:动态添加属性function Point(x, y) { this.x = x; if (y !== undefined) { this.y = y; // 有时没有 y }}
// 好:固定形状function Point(x, y = 0) { this.x = x; this.y = y; // 始终有 y}7.3 避免隐藏类转换
// 不好:改变对象形状const obj = { a: 1 };obj.b = 2; // 添加属性,创建新隐藏类delete obj.a; // 删除属性,再创建新隐藏类
// 好:提前定义所有属性const obj = { a: 1, b: null };obj.b = 2; // 不改变隐藏类7.4 优化数组访问
// 不好:混合类型数组const arr = [1, 2, "a", {}]; // 存储为对象数组
// 好:同质数组const arr = [1, 2, 3, 4]; // 存储为 SMI 数组const arr2 = [1.1, 2.2, 3.3]; // 存储为 double 数组八、调试与分析
8.1 查看字节码
# Node.js 查看字节码node --print-bytecode script.js
# 查看优化信息node --trace-opt script.js
# 查看去优化信息node --trace-deopt script.js8.2 Chrome DevTools
Performance 面板:├── 查看函数执行时间├── 识别热点函数└── 分析 GC 暂停
Memory 面板:├── 堆快照├── 分配时间线└── 内存泄漏检测8.3 性能分析代码
// 使用 performance.now() 测量const start = performance.now();for (let i = 0; i < 1000000; i++) { process(arr);}const end = performance.now();console.log(`耗时: ${end - start}ms`);
// 使用 console.timeconsole.time("process");for (let i = 0; i < 1000000; i++) { process(arr);}console.timeEnd("process");总结
V8 执行流程图
flowchart TB
A[JavaScript 源码] --> B[解析器 Parser]
B --> C[AST]
C --> D[字节码生成器]
D --> E[字节码]
E --> F[Ignition 解释器]
F --> G[执行]
G --> H[收集类型反馈]
H --> I{热点函数?}
I -->|是| J[TurboFan 编译]
I -->|否| F
J --> K[优化代码]
K --> L{假设失效?}
L -->|是| M[去优化]
M --> F
L -->|否| K
关键要点
- 解析:懒解析提升启动速度
- 字节码:中间表示,跨平台兼容
- 解释执行:快速启动,收集类型反馈
- JIT 编译:热点优化,接近原生性能
- 去优化:假设失效时的安全回退
- 内存管理:分代 GC,高效回收
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
JavaScript 引擎原理:V8 执行流程
https://blog.souloss.com/posts/principles/javascript-engine-principles/ 部分信息可能已经过时
相关文章 智能推荐
1
Python 解释器原理:CPython 执行流程
原理 深入剖析 CPython 解释器执行 Python 代码的完整流程——从源码解析到字节码生成,从虚拟机执行到内存管理,揭示 Python 运行的底层原理。
2
Java JVM 运行机制:从 .class 到机器码
原理 深入剖析 JVM 的完整运行流程——从类加载到字节码执行,涵盖运行时数据区、垃圾回收、JIT 编译等核心机制。
3
V8 引擎深入
编译器 深入 V8 引擎的核心架构——Ignition 解释器、TurboFan 优化编译器、内联缓存与隐藏类、Sparkplug 基线编译器——JavaScript 如何从解释执行到接近原生性能。
4
Go 可执行文件深度解析:ELF 结构与 runtime 嵌入
golang 深度解析 Go 编译产物的 ELF 结构——段布局、符号表、runtime 如何被打包进可执行文件、以及程序加载的全过程
5
Go GC 机制深度解析
golang 深入解析 Go 垃圾回收机制——从 GC 触发条件到四个 GC 阶段(sweep termination、并发 mark、mark termination、sweep),结合 Go runtime 源码讲解三色标记法、写屏障与 GOGC 调优参数。






