Java 是编译型还是解释型语言?
结论先行:Java 两者都是。javac 先把源码静态编译成与平台无关的字节码(bytecode),这一步是编译;运行时字节码先由**解释器(Interpreter)**逐条执行,热点代码再由 **JIT 编译器(Just-In-Time Compiler)**编译成本地机器码,这一步是解释加即时编译。所以准确说法是「编译 + 解释 + 运行时编译」的混合执行模型。
一、四个执行形态的对比
| 形态 | 代表 | 编译时机 | 启动速度 | 峰值性能 |
|---|---|---|---|---|
| 纯编译 | C / C++ / Rust | 运行前,一次编译到底 | 快(无编译开销) | 高 |
| 纯解释 | 早期 Python / Shell | 不编译,逐条翻译 | 慢 | 低 |
| 混合(Java) | HotSpot | 先编译字节码,运行时再编译热点 | 中等 | 接近纯编译 |
| 提前编译 | GraalVM Native Image | 构建期编译为可执行文件 | 极快 | 高但无运行期画像 |
二、执行链路
Hello.java
│ javac(编译期:词法/语法/语义分析,生成字节码)
▼
Hello.class(平台无关的字节码 + 常量池 + 符号引用)
│ java 启动,类加载器加载、验证、解析
▼
┌──────────── HotSpot 执行引擎 ────────────┐
│ 解释器:逐条翻译字节码,立即执行 │
│ C1(Client):快速编译,带少量优化 │
│ C2(Server):激进优化,依赖运行期画像 │
└──────────────────────────────────────────┘
│ 方法调用计数器 / 回边计数器达到阈值
▼
热点方法 → 编译为本地机器码 → 后续调用直接执行
三、热点探测与分层编译
HotSpot 用计数器判断热点:方法调用计数器统计方法被调用次数,回边计数器统计循环体回跳次数,两者之和超过阈值即提交编译。JDK 7 之后默认开启分层编译(Tiered Compilation),把编译分成 5 层。
| 层级 | 执行方式 | 特点 |
|---|---|---|
| 0 | 解释执行 | 收集调用次数与类型画像 |
| 1 | C1 编译,无 profiling | 简单方法直接定型 |
| 2 | C1 编译,带轻量 profiling | 过渡层 |
| 3 | C1 编译,带完整 profiling | 为 C2 采集数据 |
| 4 | C2 编译 | 最优代码,可去优化(deoptimization)回退 |
// 同一段代码在两个阶段的表现可能完全不同
public class JitWarmup {
public static int sum(int n) {
int s = 0;
for (int i = 0; i < n; i++) {
s += i; // 循环回边被计数,容易被 C2 优化成等差公式
}
return s;
}
public static void main(String[] args) {
long t0 = System.nanoTime();
long r = 0;
for (int i = 0; i < 200_000; i++) {
r += sum(1000); // 前几万次是解释执行,之后才走编译后的代码
}
System.out.println(r + " in " + (System.nanoTime() - t0) / 1_000_000 + "ms");
}
}
四、观察编译行为
# 打印哪些方法被编译、编译耗时与层级
java -XX:+PrintCompilation -cp . JitWarmup | head -20
# 只看 C2 的编译统计
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:-TieredCompilation -cp . JitWarmup
# 强制三种执行模式对比
java -Xint -cp . JitWarmup # 纯解释,慢一个数量级
java -Xcomp -cp . JitWarmup # 全部编译,启动极慢
java -Xmixed -cp . JitWarmup # 默认,混合
PrintCompilation 的输出里,% 表示走了 OSR(栈上替换,循环跑到一半被编译),made not entrant 表示去优化——常见于类型画像失效,比如同一个接口突然出现了新实现。
五、为什么字节码不直接编译成机器码
| 原因 | 说明 |
|---|---|
| 跨平台 | 字节码是 JVM 规范的中间表示,换个平台的 JVM 即可运行,无需重新编译 |
| 安全 | 加载期可做字节码验证,拒绝越界访问、类型混淆等非法指令 |
| 动态性 | 运行期才知道类路径、实现类、CPU 特性,JIT 能据此做更激进的优化 |
| 可观测 | 字节码可被 agent 改写,实现链路追踪、热部署、mock |
六、常见追问
| 追问 | 回答要点 |
|---|---|
| JIT 比静态编译强在哪 | 有运行期画像:能内联虚方法、按实际类型去虚化、按 CPU 指令集生成代码;静态编译只能保守假设 |
| JIT 一定更快吗 | 不一定。启动阶段是解释执行,长时间预热后才反超;短生命周期进程反而是劣势 |
| 什么是去优化 | 编译时的乐观假设被推翻(如出现新子类),JVM 丢弃已编译代码回退到解释执行 |
| AOT 和 JIT 的关系 | AOT 在构建期完成编译,没有运行期画像,靠 PGO 弥补;与 JIT 是互补而非替代 |
为什么 -Xint 还有人用 | 排查「JIT 优化引入的 bug」、复现只在编译后出现的竞态,或用它反证问题与编译相关 |
小结
Java 的「编译」指的是 javac 到字节码那一步,「解释」和「即时编译」发生在运行时。理解这套混合模型,才能解释线上为什么会有预热慢、去优化抖动、PrintCompilation 输出异常这些现象。