编译 vs 解释运行(机器码/指令/汇编)
两条路:编译执行与解释执行
理解 JVM 的执行方式,需要先理解计算机执行代码的两条基本路径:编译执行和解释执行。
编译执行:提前把所有代码翻译好
编译型语言(如 C、C++、Go)在程序运行之前,先把源代码一次性全部翻译成本地机器码,生成可执行文件。运行时直接执行机器码。
源代码 (.c) ──→ 编译器 ──→ 可执行文件 (.exe)
│
▼
直接交给 CPU 执行
(不再需要编译器)编译型语言的特点
| 特点 | 说明 |
|---|---|
| 执行速度 | 快(直接执行机器码) |
| 启动速度 | 快(不需要编译步骤) |
| 跨平台 | 差(需要针对每个平台分别编译) |
| 内存占用 | 小(没有运行时解释器) |
| 典型代表 | C、C++、Go、Rust |
解释执行:边跑边翻译
解释型语言(如 Python、Ruby、JavaScript)在程序运行过程中,由解释器逐条读取源代码或中间表示,逐条翻译成机器码并执行。
源代码 (.py) ──→ 解释器 ──→ 读取一行
│ 翻译一行
▼ 执行一行
│
← (循环)解释型语言的特点
| 特点 | 说明 |
|---|---|
| 执行速度 | 慢(每次都需要翻译) |
| 启动速度 | 快(不需要预先编译) |
| 跨平台 | 好(只要有解释器) |
| 内存占用 | 大(需要解释器常驻) |
| 典型代表 | Python、Ruby、JavaScript |
Java 的特殊路径:中间字节码
Java 走了一条中间路线:
源代码 (.java)
│
▼
前端编译器(javac)
│
▼
字节码 (.class)
│
├──→ 解释执行(启动时)
│
└──→ JIT 编译执行(热点代码)
│
▼
本地机器码
│
▼
CPU 执行Java 既不是纯编译型,也不是纯解释型。它的字节码是「编译+解释」的产物:
- javac:把
.java编译成.class(编译阶段) - JVM 解释器:把
.class解释成机器码(解释执行) - JIT 编译器:把热点
.class编译成本地机器码(编译执行)
机器码、字节码、汇编
这三个概念经常被混淆,一图说清楚:
┌──────────────────────────────────────────────────────────────┐
│ 编译执行 vs 解释执行 │
│ │
│ C 代码 ──→ 编译器 ──→ 机器码 ──→ CPU 直接执行 │
│ (运行前) (010101...) │
│ │
│ Java 代码 ──→ javac ──→ 字节码 ──→ 解释器 ──→ 机器码 ──→ CPU │
│ (运行前) (class) (运行时) (010101...) │
│ │
│ 字节码:JVM 的「机器码」,面向 JVM,不针对任何具体 CPU │
│ 机器码:操作系统的「原生码」,针对具体 CPU 架构(x86/ARM) │
└──────────────────────────────────────────────────────────────┘| 概念 | 说明 | 例子 |
|---|---|---|
| 机器码 | CPU 能直接执行的二进制指令,0101... | 0x55 0x48 0x8B |
| 字节码 | JVM 的中间指令,面向 JVM,不针对具体硬件 | iconst_1、iload_2 |
| 汇编 | 机器码的文本表示,一一对应 | mov rax, rbx |
字节码与机器码的区别
| 维度 | JVM 字节码 | 本地机器码 |
|---|---|---|
| 目标平台 | JVM(软件抽象) | x86/ARM/RISC-V(硬件) |
| 存储格式 | .class 文件 | 可执行文件 |
| 指令长度 | 变长(1~9 字节) | 固定/变长(取决于架构) |
| 架构 | 统一(所有平台相同) | 不统一(x86 ≠ ARM) |
| 执行方式 | 解释或 JIT 编译 | 直接执行 |
| 可移植性 | 高(只依赖 JVM) | 低(依赖 CPU) |
字节码的具体形态
用 javap -c 看看字节码长什么样:
java
public class ByteCodeDemo {
public static int add(int a, int b) {
return a + b;
}
}编译后:
java
public static int add(int, int);
Code:
0: iload_0 // 加载局部变量 slot 0(参数 a)
1: iload_1 // 加载局部变量 slot 1(参数 b)
2: iadd // 相加
3: ireturn // 返回 int对应的 x86 机器码可能是(objdump 反汇编):
asm
push %rbp
mov %rsp,%rbp
mov %edi,-0x4(%rbp) ; 存 a
mov %esi,-0x8(%rbp) ; 存 b
mov -0x4(%rbp),%eax ; 取 a
add -0x8(%rbp),%eax ; 加 b
pop %rbp
ret编译优化:JIT 的威力
JIT 编译器之所以比纯解释执行快,一个重要原因是编译时可以进行大量优化:
java
public class JITOptimization {
static int compute() {
int sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i;
}
return sum;
}
}解释执行时,JVM 每次循环都要:取指令 → 解码 → 查表 → 执行。
JIT 编译后,这段代码可能变成直接执行编译后的机器指令,没有了解释的中间开销。而且 JIT 编译器还会做更多优化:
| 优化类型 | 说明 |
|---|---|
| 内联 | 把方法调用替换为方法体,减少调用开销 |
| 死代码消除 | 删除永远不会执行的代码 |
| 常量折叠 | 把 1 + 2 直接替换成 3 |
| 逃逸分析 | 分析对象是否逃逸,决定栈上分配还是堆分配 |
| 向量化 | 将多个标量操作合并成 SIMD 向量指令 |
预编译(AOT):编译的另一种形态
JDK 9 引入的 AOT(Ahead-Of-Time)编译,是第三条路:
源代码 (.java)
│
▼
jaotc 编译器(JDK 9+)
│
▼
本地机器码(.so / .dll)
│
▼
JVM 加载时直接使用预编译的机器码
(跳过 JIT 编译阶段)bash
# JDK 9+ 使用 jaotc 预编译
jaotc --output lib.so MyApp.class
java -XX:AOTLibrary=./lib.so MyAppAOT 和 JIT 的对比:
| 维度 | JIT 编译 | AOT 编译 |
|---|---|---|
| 编译时机 | 运行时(程序运行中) | 运行时之前(程序启动前) |
| 编译优化 | 基于运行时信息(profile-guided) | 无运行时信息 |
| 启动速度 | 慢(需要预热) | 快(直接使用预编译代码) |
| 峰值性能 | 高(JIT 可根据运行时信息优化) | 较低(无运行时反馈) |
| 适用场景 | 服务器长期运行应用 | 短期程序、容器镜像 |
本节小结
编译与解释的三种形态:
| 模式 | 编译时机 | 代表 | 特点 |
|---|---|---|---|
| 纯编译 | 运行前 | C、C++ | 快,但不可移植 |
| 纯解释 | 运行时 | Python、Ruby | 可移植,但慢 |
| Java 混合 | 运行时 | Java(解释+JIT) | 启动快 + 峰值快 |
Java 的字节码是「可移植的中间态」,JIT 编译器在此基础上实现「启动快 + 运行快」的双重目标。
下一节,我们来看 解释器与 JIT 编译器并存原因。
