一、两次编译,不是绕远路
Java 程序天然经历两次编译:
-
前端编译
javacJava 源码 →
.class字节码 -
后端编译
JVM 执行引擎
字节码 → 本地机器指令
前端编译解决"正确 "。
后端编译解决"快"。
这不是历史包袱,是设计选择。
二、前端编译:忠于逻辑,克制优化
javac 做优化,但很保守。
1. 死代码消除
java
if (false) {
System.out.println("never");
}
字节码里消失。
这是确定性优化。
2. try-catch-finally 的真实面目
java
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3;
}
字节码结果:
- try 的 return 被抹掉
- catch 的 return 被抹掉
- 只保留 finally 的 return
实现方式很"笨":
复制 finally 三次。
- try 后一份
- catch 后一份
- 方法末尾一份
目的只有一个:finally 一定执行。
3. 为什么不做更多优化?
java
Object obj = null;
if (obj != null) {
// 永远走不到
}
javac 看得出,人也看得出。
它还是保留。
原因很简单:
JVM 不是 Java 专用的。
Scala、Kotlin、Groovy、Clojure
它们只要能生成合法 .class,就能跑。
如果前端编译器过度聪明:
- 其他语言的编译器成本爆炸
- JVM 生态直接坍塌
结论 :
前端编译优先保证语义,不抢性能的活。
三、解释执行:最原始的 JVM
最早的 JVM 很直接:
来一条字节码
翻译一条机器指令
执行
下一条
优点:
- 简单
- 启动快
- 占用少
缺点:
- 慢
就像把"人山人海"翻译成:
people mountain people sea
对,但没用。
四、为什么不直接编译成机器码?
这是 C / C++ 的路线。
优点:
- 快
- 精准
代价:
- 不跨平台
- 构建复杂
- 部署成本高
Java 赌的是另一条路:
运行时知道得更多
于是,JIT 出现了。
五、即时编译(JIT):只为热点而生
核心思想很简单:
冷代码解释执行
热代码编译缓存
Code Cache
- 字节码 → 机器码
- 存在内存里
- 下次直接跑
这就是 HotSpot 名字的来源。
六、混合模式,才是默认选择
JVM 的三种执行模式:
-Xint:纯解释-Xcomp:纯编译- Mixed Mode(默认)
为什么不是全编译?
编译执行有成本:
- 热点探测
- 编译耗时
- Code Cache 占内存
- 需要"预热"
短命程序:
- CLI
- 脚本
- Serverless 冷启动
解释执行反而更快。
七、热点代码如何识别?
JVM 用两把尺子。
1. 方法调用计数器
- 每个方法一个计数器
- 默认阈值:10,000 次
- 超过 → 编译
参数查看:
bash
-XX:+PrintFlagsInitial
关键参数:
CompileThreshold = 10000
2. 回边计数器(Back Edge)
解决这个问题:
java
for (;;) {
x++;
}
没有方法调用,只有循环。
字节码层面靠什么识别?
goto 指令
- 跳回
- 再跳回
- 次数够了 → 编译这一段
Server 模式默认阈值 ≈ 10700
八、OSR:运行中替换
热点编译不是等方法结束。
而是:
On-Stack Replacement
正在跑的解释执行代码
→
无缝切到编译版本
不停机。
不重来。
九、JIT 的真正价值:激进优化
一旦进入 JIT 世界,规则变了。
常见优化包括:
- 方法内联
- 逃逸分析
- 锁消除
- 锁粗化
- 循环展开
- 分支预测
这些:
- 依赖运行时信息
- 依赖真实数据
- 前端编译器做不了
十、为什么 Java 能追上 C++?
答案很直白:
它在你跑的时候学习你
- 热路径
- 实际分支
- 真正用到的类型
不是猜,是观察。
十一、实践(不常见)
- 短任务:别纠结 JIT
- 长服务:关注预热
- Benchmark:一定要热身
- 调优前:先看 JIT 日志
bash
-XX:+PrintCompilation
-XX:+LogCompilation