Why this matters

Java 代码看起来离底层很远,但真正交给 JVM 执行时,面对的并不是 .java 文件,而是 .class 字节码。

很多问题只有把源码、编译结果和字节码执行过程对上之后才会真正看明白。比如常量池里到底存了什么,简单表达式为什么会在编译期折叠,方法调用在字节码层是怎么展开的,这些都需要借助反汇编工具来观察。

Bytecode inspection is the fastest way to build JVM intuition

Java 反汇编并不是把 .class 文件“还原成源码”,而是把它转成一种更适合阅读的形式,让你看到:

  • 类的版本号和基础元数据
  • 常量池里的符号引用
  • 方法体对应的字节码指令
  • 局部变量表和行号表

这类信息对学习 JVM、排查编译行为和理解框架生成代码都很有帮助。你不一定需要每天都看字节码,但只要遇到“编译器到底帮我做了什么”的问题,反汇编通常是最快的答案来源。

javap is the lowest-friction entry point

最直接的工具是 JDK 自带的 javap。先看帮助即可知道它大概能输出哪些信息:

Bash
javap --help

常用参数里,最实用的几项通常是:

  • -c:反汇编方法体
  • -l:显示行号和本地变量表
  • -s:显示类型签名
  • -v:输出更完整的详细信息

如果你只是想快速确认某个 .class 文件到底编译成了什么,通常直接用 -v 就够了。

A minimal example is enough to看出编译器做了什么

比如有下面这样一段简单代码:

Java
public class TestJavap {
    public static void main(String[] args) {
        int a = 1 + 2;
        System.out.println(a);
    }
}

先编译它:

Bash
javac TestJavap.java

然后查看字节码:

Bash
javap -v TestJavap.class

你会看到一个关键信号:源码里写的是 1 + 2,但字节码里并不会真的逐步做加法,而是直接出现一个常量结果:

Text
0: iconst_3
1: istore_1
2: getstatic     #2
5: iload_1
6: invokevirtual #3
9: return

这说明编译器已经在编译阶段把 1 + 2 折叠成了常量 3。反汇编最大的价值之一,就是把这种“源码表面看不到,但编译器实际做了”的事情直接展示出来。

What to read first in a javap -v dump

第一次打开完整反汇编输出时,信息量会很多。更高效的读法不是从头硬扫,而是先抓下面这几块。

1. 全局元数据

全局元数据会告诉你这个类文件最基础的背景信息,比如:

  • 编译来源的源文件名
  • 文件大小
  • 最后修改时间
  • 主版本号和次版本号
  • 访问标志

其中最值得记住的是 major version。它能直接映射到 Java 版本,例如:

JDK 版本Major Version
JDK 852
JDK 1155
JDK 1761
JDK 2165

所以当你排查“某个类到底是不是用目标版本编译出来的”时,这一块非常直接。

2. 常量池

常量池不是只放字符串常量,它更像类文件里的“索引中心”,里面记录了:

  • 类引用
  • 方法引用
  • 字段引用
  • 方法名与描述符
  • 各种 Utf8 字面量

例如:

Text
#1 = Methodref  #5.#14   // java/lang/Object."<init>":()V
#2 = Fieldref   #15.#16  // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref  #17.#18  // java/io/PrintStream.println:(I)V

这几项就足以把一个简单 main 方法里的构造、字段访问和打印调用串起来。

如果你刚开始看常量池,最值得先认清的是描述符写法:

  • I 表示 int
  • V 表示 void
  • Ljava/lang/String; 表示 String
  • [Ljava/lang/String; 表示 String[]

只要把描述符读顺了,方法签名就不再难看。

3. 方法区

方法区是最接近“实际执行过程”的部分。每个方法下通常会给出:

  • descriptor
  • flags
  • Code
  • LineNumberTable

还是拿上面的 main 举例:

Text
public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=2, args_size=1
       0: iconst_3
       1: istore_1
       2: getstatic     #2
       5: iload_1
       6: invokevirtual #3
       9: return

读这段时,可以按执行顺序理解:

  1. iconst_3 把常量 3 压入操作数栈
  2. istore_1 把它存入局部变量槽位
  3. getstatic 取出 System.out
  4. iload_1 再把局部变量加载回来
  5. invokevirtual 调用 println
  6. return 结束方法

当你用这种“操作数栈 + 局部变量表”的视角去看字节码时,很多 JVM 指令会一下子变得直观。

jclasslib is better when you want a GUI view

如果只是偶尔看一下,javap 已经够用;但如果你想在 IDE 里更稳定地浏览类结构,jclasslib 会更舒服一些。

它的优点主要是:

  • 不用在终端里翻长文本
  • 常量池、字段、方法、属性区可以直接分栏查看
  • 很适合对照着源码一点点点进去

它尤其适合下面这类场景:

  • 想确认一个类在编译后到底带了哪些属性
  • 想直接浏览常量池,而不是在文本里查索引
  • 想看局部变量表、行号表等结构化信息

前提也一样:你得先让类编译出来,不管是 IDE 构建、运行一次程序,还是手动执行 javac。

Practical checks

如果你是第一次开始把反汇编作为日常排查工具,建议优先检查这些问题:

  • 先确认 .class 文件是不是你以为的那个编译结果
  • 先看 major version,确认目标 JDK 是否正确
  • 再看常量池,确认方法和字段引用是否符合预期
  • 最后再看方法区里的 Code,把每条指令对应回源码语义

如果想快速判断编译器有没有做优化或折叠,也优先去方法区看字节码,而不是先猜。

Closing notes

Java 反汇编真正有用的地方,不是“学会几条 JVM 指令”,而是建立一种观察习惯:当源码和运行结果之间有一层看不见的编译行为时,就直接去看字节码。

javap 适合最低成本地进入这个视角,jclasslib 适合更稳定地浏览类文件结构。只要你开始习惯看常量池、描述符和方法区,很多 JVM 相关的问题都会比单看源码更容易解释。

感谢读到这里

日志还会继续,欢迎常来。

返回首页