`javac` 与 `javap`:从源码到字节码,定位“编译器到底生成了什么”
·
javac 与 javap:从源码到字节码,定位“编译器到底生成了什么”
当你遇到这些问题时,javac/javap 是最短的证据链:
- 为啥一个语法糖(lambda、try-with-resources)会变成一堆类/方法?
- 为什么这里发生了拆箱 NPE?
- 为什么 finally 里会覆盖异常?
- 为什么泛型被“擦除”后还能跑?
本文用最小例子带你读懂编译产物与常见字节码模式。
0. 一句话结论
javac:把.java编译成.classjavap:把.class反汇编,看到常量池、方法签名、字节码指令(以及编译器合成方法)
0.1 最小可复制命令:从源码到字节码(不依赖 IDE)
# 1) 编译
javac -g -d out src/Hello.java
# 2) 反汇编(含详细信息)
javap -classpath out -v Hello | sed -n '1,120p'
你重点关注:
- 是否出现了编译器合成方法(synthetic/bridge)
- 是否出现拆箱调用(
Integer.intValue()) - 异常表与 finally/try-with-resources 的处理结构
1. 可复制示例:拆箱 NPE 是怎么来的
源码:
Integer x = null;
int y = x; // NPE
你用 javap 会看到类似“调用 intValue()”的模式,这解释了:拆箱本质是方法调用,所以目标为 null 就 NPE。
2. 可复制示例:try-with-resources 生成了什么
语法糖:
try (InputStream in = ...) {
// ...
}
编译器会生成:
- 自动 close
- suppressed 异常处理逻辑
这解释了为什么某些异常栈里会出现 suppressed。
3. 可复制示例:lambda 为什么会变成合成方法/ invokedynamic
lambda 并不等于“匿名内部类”:
- 编译器会生成 invokedynamic 引导
- 运行时再生成对应实现
这也解释了为什么 lambda 的堆栈与调试符号有时不直观。
4. 真实场景库(你什么时候该用 javap)
- 定位语义差异:某段代码在不同 JDK 行为不一致(看编译产物差异)
- 理解性能:某处大量装箱/拆箱(看字节码有没有隐式转换)
- 排查异常:finally/try-with-resources 的异常覆盖/抑制(看异常处理表)
4.1 高发:泛型擦除导致的桥接方法(bridge method)
现象:
- 反射看到的方法签名和你写的不一致
- 某些代理/框架调用“看起来奇怪的方法”
根因:编译器为兼容泛型擦除会生成 bridge method。
下一步动作:
- 用
javap -v搜索bridge/synthetic - 明确框架层是否会扫描到桥接方法(并做过滤)
5. checklist
- 你看到的“神秘行为”是否可能来自语法糖?
- 是否存在隐式拆箱/装箱?
- 是否存在合成方法(桥接方法)导致反射/代理行为不同?
6. 小结
javac 负责“生成现实”,javap 负责“揭示现实”。当你需要证据解释“为什么会这样执行”,读字节码往往比猜测更快闭环。
更多推荐


所有评论(0)