07 - 执行引擎与 JIT
定位: 执行引擎是 JVM 的"CPU"——将字节码转换为机器码并执行。解释执行保证快速启动,JIT 编译保证峰值性能。 面试高频度: ⭐⭐⭐⭐⭐
一、执行引擎在 JVM 体系中的位置
类加载器将 Class 文件加载到内存后,执行引擎负责真正执行这些代码:
┌─────────────────────────────────────────────────────────────────────────┐
│ JVM 整体架构 │
│ │
│ 源码 ─→ 编译 ─→ Class文件 ─→ 类加载 ─→ 运行时数据区 ─→ 执行引擎 ─→ 机器码 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 方法区 │ │ 解释器 │ │
│ │ 堆 │ │ C1 编译器 │ │
│ │ 栈 │ │ C2 编译器 │ │
│ └──────────┘ └──────────┘ │
│ │
│ ~~~~~ 静态部分 (编译期+加载期) ~~~~~ ~~~~~ 动态部分 (运行期) ~~~~~ │
└─────────────────────────────────────────────────────────────────────────┘- 向上: 从运行时数据区读取字节码指令(方法区中的 Code 属性)
- 向下: 将字节码翻译为机器码,在目标 CPU 上执行
- 横向: 与运行时数据区双向交互——栈帧操作、堆对象分配、方法调用
二、子专题导航
| # | 主题 | 面试频率 | 核心内容 |
|---|---|---|---|
| **解释执行与编译执行** | 混合执行模式 | ⭐⭐⭐⭐ | 解释器 vs 编译器、热点检测、OSR、CompileThreshold |
| **C1与C2编译器与分层编译** | JIT 编译器 | ⭐⭐⭐⭐⭐ | C1 vs C2 优化管线、5 级分层编译、去优化 (Deopt) |
| **方法内联和逃逸分析** | 核心 JIT 优化 | ⭐⭐⭐⭐⭐ | 方法内联(最重要优化)、逃逸分析、虚调用优化 |
| **锁消除与标量替换** | 高级 JIT 优化 | ⭐⭐⭐⭐ | 锁消除、标量替换、StringBuffer 案例、条件分析 |
| **JITWatch使用** | JIT 分析工具 | ⭐⭐⭐ | JIT 日志生成、TriView、内联报告、性能瓶颈定位 |
三、核心概念全景
┌──────────────────────────────────────────────────────────────────────────┐
│ JIT 编译全景流程图 │
│ │
│ 字节码 (bytecode) │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 解释执行 (解释器) │──→ 立即启动, 无编译等待 │
│ │ - 模板解释器 │ - 每条字节码对应一段 native 模板 │
│ │ - 无优化 │ - 执行速度约 1/10 ~ 1/100 编译后代码 │
│ └──────┬───────────────┘ │
│ │ 计数器到达阈值 │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 热点检测 │ │
│ │ - 方法调用计数器 │ │
│ │ - 回边计数器 │ │
│ │ - 计数器衰减 │ │
│ └──────┬───────────────┘ │
│ │ 热点识别完毕 │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 分层编译 (Tiered Compilation) │ │
│ │ ┌──────────────────────────────────┐│ │
│ │ │ Level 0: 纯解释执行 (无编译) ││ │
│ │ │ Level 1: C1 无 profiling ││ │
│ │ │ Level 2: C1 基础 profiling ││ │
│ │ │ Level 3: C1 完整 profiling ││ │
│ │ │ Level 4: C2 完整优化 ││ │
│ │ └──────────────────────────────────┘│ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ JIT 核心优化 │ │ 去优化 (Deopt) │ │
│ │ - 方法内联 (最重要) │──→ 假设 │ - 类加载导致 │ │
│ │ - 逃逸分析 │ 不成立 │ - 类型变化 │ │
│ │ - 锁消除 │ │ - 回退到解释执行 │ │
│ │ - 标量替换 │ │ - 重新编译 │ │
│ │ - 循环优化 │ └──────────────────┘ │
│ │ - 边界检查消除 │ │
│ └──────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘四、核心考点速记
1. 解释执行 vs 编译执行
| 维度 | 解释执行 (Interpreter) | 编译执行 (JIT Compiler) |
|---|---|---|
| 启动速度 | 极快(无编译延迟) | 慢(需要预热) |
| 峰值性能 | 低(~1/10 编译代码) | 高(接近 native 代码) |
| 内存消耗 | 低 | 高(CodeCache 存储编译代码) |
| 优化能力 | 无优化 | C1 ~100 种优化,C2 ~1000 种优化 |
| 适用场景 | 冷代码、罕见路径 | 热点代码、频繁执行路径 |
| 关键参数 | 无 | -XX:CompileThreshold=10000 |
2. C1 与 C2 对比
| 维度 | C1 (Client) | C2 (Server) |
|---|---|---|
| 编译速度 | 快(<50ms/方法) | 慢(>1s/方法) |
| 优化强度 | 中等(~100 种优化) | 激进(~1000 种优化) |
| 逐方法耗时 | 亚毫秒级 | 秒级 |
| 代码质量 | 中等 | 极高 |
| CodeCache 占用 | 较少 | 较多 |
3. 逃逸分析三件套
| 优化 | 作用 | 效果 |
|---|---|---|
| 栈上分配 (Stack Allocation) | 非逃逸对象 → 栈上分配 | 零 GC 开销 |
| 标量替换 (Scalar Replacement) | 非逃逸对象 → 分解为标量字段 | 对象完全消除 |
| 锁消除 (Lock Elimination) | 非逃逸对象 → 移除同步 | 线程安全锁自动消除 |
4. 分层编译 5 级
| 层级 | 名称 | 编译方式 | profiling | 说明 |
|---|---|---|---|---|
| 0 | 解释执行 | 无编译 | 无 | 纯解释,启动阶段 |
| 1 | C1 简单 | C1 编译 | 无 | 简单 C1,不收集 profile |
| 2 | C1 基础 | C1 编译 | 调用/回边计数 | 有限 profiling |
| 3 | C1 完整 | C1 编译 | 类型 + 分支 profile | 为 C2 做准备 |
| 4 | C2 极限 | C2 编译 | 使用 L3 profile | 极限优化 |
五、面试高频追问一览
| 追问 | 关联笔记 | 频次 |
|---|---|---|
| 为什么需要解释器和编译器共存? | **解释执行与编译执行** | 极高 |
| JIT 如何判断哪些代码需要编译? | **解释执行与编译执行** | 高 |
| 什么是分层编译?为什么需要 5 级? | **C1与C2编译器与分层编译** | 极高 |
| C1 和 C2 有什么区别? | **C1与C2编译器与分层编译** | 高 |
| 什么是去优化 (Deoptimization)? | **C1与C2编译器与分层编译** | 高 |
| 方法内联为什么是最重要的 JIT 优化? | **方法内联和逃逸分析** | 极高 |
| 逃逸分析如何工作? | **方法内联和逃逸分析** | 极高 |
| 标量替换和栈上分配有什么区别? | **锁消除与标量替换** | 高 |
| 什么是锁消除? | **锁消除与标量替换** | 高 |
| 如何分析 JIT 编译日志? | **JITWatch使用** | 中 |
📚 相关链接
- **字节码指令集概览** — 字节码是执行引擎的操作对象
- **分配策略(栈上分配与TLAB与大对象)** — 逃逸分析的实战效果
- **synchronized的JVM实现** — 锁消除的底层原理衔接
- **JVM文库设计文档**