Skip to content

V8 引擎与 JIT 编译

1. V8 执行流水线


2. Ignition 解释器

  • 将 AST 编译为字节码 (Bytecode)
  • 字节码是一种中间表示,比 AST 更紧凑
  • 启动速度快,但执行效率不如机器码

为什么先解释执行?

  • 快速启动: 无需等待编译优化,JS 代码可以直接运行。
  • 收集类型信息 (Feedback Vector):
    • Ignition 在执行字节码时,会收集函数的输入类型、对象结构等信息。
    • 这些信息存储在 Feedback Vector 中。
    • TurboFan 后续会根据这些反馈信息进行"投机性优化" (Speculative Optimization)。

3. TurboFan 优化编译器

触发条件

函数被调用次数超过阈值 (热点检测) → 触发 TurboFan 编译

核心优化技术

内联 (Inlining)

将小函数直接嵌入调用处,消除函数调用开销。

逃逸分析 (Escape Analysis)

分析对象是否逃逸出函数作用域,未逃逸则可栈分配 (无 GC 压力)。

类型特化 (Type Specialization)

基于收集的类型信息,生成针对特定类型的优化代码。


4. 隐藏类 (Hidden Class / Map)

问题

JavaScript 对象是动态的,属性可随时增删。如何快速访问属性?

解决方案: 隐藏类

V8 为每个对象创建隐藏类 (Map),描述对象的结构 (属性名、偏移量)。

javascript
// 对象 a 和 b 共享同一个隐藏类
const a = { x: 1, y: 2 };
const b = { x: 3, y: 4 };

// 对象 c 属性顺序不同,隐藏类不同!
const c = { y: 5, x: 6 };

隐藏类转换链

最佳实践

  1. 保持属性添加顺序一致: 不同顺序会导致创建不同的隐藏类分支。
  2. 避免运行时删除属性: delete 操作会导致对象回退到字典模式 (Dictionary Mode),极慢。
  3. 避免动态添加属性: 尽量在构造函数中一次性声明所有属性。

深入: 对象属性存储

V8 根据属性数量和动态性选择存储方式:

  • In-object Properties: 直接存储在对象头中 (最快,通常限制约 10 个)。
  • Fast Properties: 存储在属性数组中 (次快)。
  • Dictionary Mode: 哈希表存储 (慢,用于删除了属性或属性极多的对象)。

5. 内联缓存 (Inline Cache, IC)

问题

每次属性访问都查询隐藏类,效率低。

解决方案

缓存上次属性访问的隐藏类和偏移量。

IC 状态变迁

IC 的状态会根据遇到的对象隐藏类数量发生变化:

NOTE

性能差异巨大:单态 IC 比多态 IC 快得多,而超态 IC (Megamorphic) 会回退到查表操作。

最佳实践

保持对象形状一致:

  • 避免同一个函数处理不同结构的对象。
  • 尤其是数组: [1, 2] (PACKED_SMI) 和 [1.1, 2] (PACKED_DOUBLE) 是不同 Map。

6. Garbage Collection (Orinoco)

V8 的垃圾回收器名为 Orinoco,基于分代回收 (Generational Collection) 策略。

内存分代

V8 将堆内存分为新生代 (New Space)老生代 (Old Space)

1. 新生代 (Minor GC - Scavenger)

  • 对象特征: 存活时间短 (如临时变量)。
  • 算法: Cheney 算法 (复制算法)。
  • 过程: 将存活对象从 From-Space 复制到 To-Space,清空 From-Space。
  • 速度: 极快,但空间利用率低 (仅使用一半)。

2. 老生代 (Major GC)

  • 对象特征: 存活时间长,或内存占用大。
  • 算法:
    • Mark-Sweep (标记-清除): 标记活动对象,清除垃圾 (产生碎片)。
    • Mark-Compact (标记-整理): 整理内存碎片,移动对象。
  • 优化:
    • Incremental Marking (增量标记): 将标记过程拆解,避免长时间卡顿 (STW)。
    • Lazy Sweeping (惰性清除): 垃圾回收不立即清除所有,按需清理。

写屏障 (Write Barrier)

当老生代对象引用新生代对象时,V8 使用写屏障记录这种引用,避免 Minor GC 扫描整个老生代。


7. 优化指南与避坑总结

7.1 避免去优化 (Deoptimization)

当 TurboFan 的类型假设失效时,回退到 Ignition,会导致性能断崖式下降。

❌ 触发场景 (Don'ts)

  1. 类型不稳定:
    javascript
    let x = 1;
    x = 'string'; // ❌ 导致分配给 x 的寄存器类型改变
  2. 隐藏类突变:
    javascript
    delete obj.x; // ❌ 回退到字典模式
  3. 访问 arguments: 使用 ...args 代替 arguments 对象。

7.2 函数优化建议

  1. 保持函数体小: 小函数更容易被内联 (Inlining)。
  2. 参数类型固定: 无论单态还是多态,保持稳定都好过不断变化。
  3. 作用域纯净: 减少闭包对高频变量的捕获,利于逃逸分析。

7.3 数组优化

V8 对数组有细分的元素类型 (Elements KIND):

  • PACKED_SMI_ELEMENTS: 纯整数 (最快)
  • PACKED_DOUBLE_ELEMENTS: 浮点数
  • HOLEY_ELEMENTS: 稀疏数组 (最慢)

建议:

  • 初始化时确定大小 (避免动态扩容)。
  • 避免空洞 (Holes): arr[100] = 1 会导致 0-99 都是空洞。
  • 避免类型混合: [1, 1.5, 'a'] 会降低数组性能等级。

前端面试知识库