mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1035 字
3 分钟
JavaScript 引擎原理:V8 执行流程
2024-05-04

前言#

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 字节码指令集(部分):

指令操作
LdarLoad accumulator from register
StarStore accumulator to register
Add加法运算
Sub减法运算
Mul乘法运算
LdaGlobal加载全局变量
StaGlobal存储全局变量
Call函数调用
Return返回
Jump跳转
TestEqual相等比较

2.3 字节码生成示例#

// 源码
function sum(a, b) {
return a + b;
}
// 生成的字节码(简化)
// 参数 a 在寄存器 a0, b 在寄存器 a1
Ldar a0 // 加载 a 到累加器
Add a1, [0] // 累加器 + b,结果存入累加器
Return // 返回累加器值
// 源码:条件语句
function abs(x) {
if (x >= 0) {
return x;
}
return -x;
}
// 字节码(简化)
Ldar a0 // 加载 x
Star r0 // 存入 r0
LdaSmi [0] // 加载立即数 0
TestGreaterThanOrEqual r0 // 比较 x >= 0
JumpIfFalse [10] // 条件为假跳转到地址 10
Ldar a0 // 返回 x
Return
Ldar a0 // 加载 x
Neg // 取负
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 0
getX(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]); // 不再是优化代码

去优化的影响

  1. 丢弃已编译的优化代码
  2. 恢复到解释执行
  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 Space1-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 }; // 隐藏类 A
const p2 = { y: 1, x: 2 }; // 隐藏类 B(不同!)

七、性能优化实践#

7.1 保持类型稳定#

// 不好:类型不稳定
function process(value) {
if (typeof value === "number") {
return value * 2;
}
return value + value; // 字符串拼接
}
process(1); // number
process("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.js

8.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.time
console.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

关键要点#

  1. 解析:懒解析提升启动速度
  2. 字节码:中间表示,跨平台兼容
  3. 解释执行:快速启动,收集类型反馈
  4. JIT 编译:热点优化,接近原生性能
  5. 去优化:假设失效时的安全回退
  6. 内存管理:分代 GC,高效回收

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

JavaScript 引擎原理:V8 执行流程
https://blog.souloss.com/posts/principles/javascript-engine-principles/
作者
Souloss
发布于
2024-05-04
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时