javacjavap:从源码到字节码,定位“编译器到底生成了什么”

当你遇到这些问题时,javac/javap 是最短的证据链:

  • 为啥一个语法糖(lambda、try-with-resources)会变成一堆类/方法?
  • 为什么这里发生了拆箱 NPE?
  • 为什么 finally 里会覆盖异常?
  • 为什么泛型被“擦除”后还能跑?

本文用最小例子带你读懂编译产物与常见字节码模式。


0. 一句话结论

  • javac:把 .java 编译成 .class
  • javap:把 .class 反汇编,看到常量池、方法签名、字节码指令(以及编译器合成方法)

javac

javap -v

.java

.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 负责“揭示现实”。当你需要证据解释“为什么会这样执行”,读字节码往往比猜测更快闭环。

更多推荐