javap是OpenJDK自带最稳的字节码查看工具,-verbose输出完整结构(含魔数、版本号、常量池、Code属性等),-c仅显示方法指令,-l附加行号与局部变量表;它不反编译源码,只展示JVM真实执行的字节码。

直接用 javap,别写解析器、别碰 asmtools、更别手动读二进制 —— 这是 OpenJDK 自带且最稳的方案。
javap -verbose 是字节码查看的第一入口
它不反编译 Java 源码,而是把 class 文件里真正被 JVM 执行的指令、常量池、属性表等结构原样摊开。你看到的 iconst_1、istore_1、invokestatic 就是真实字节码操作码(opcode)的助记符。
-
javap -verbose MyClass.class:输出全部结构,包括魔数、版本号、常量池索引、字段/方法签名、Code 属性里的每条指令和行号表 -
javap -c MyClass.class:只看方法字节码指令(精简版,适合快速定位逻辑流) -
javap -l MyClass.class:额外显示局部变量表和行号映射,调试时能对齐源码位置 - 注意:类名必须是已编译的
.class文件路径,不能是包名或未编译的.java
javap 输出里哪些字段最关键
初看 javap -verbose 输出容易被常量池刷屏,但真正影响运行行为的是这几块:
-
minor version和major version:决定该 class 能否在当前 JVM 上加载。比如major version: 61表示 JDK 17 编译,JDK 11 会直接抛UnsupportedClassVersionError -
Constant pool:不是字符串列表,而是符号引用仓库。例如#1 = Methodref #2.#3表示“调用某个类的某个方法”,具体类和方法名要顺着编号查下去 -
Code:下的stack=...、locals=...:JVM 栈帧大小和局部变量槽数,直接影响方法能否执行(如局部变量超 65535 会报错) -
LineNumberTable和LocalVariableTable:仅在编译时加了-g参数才存在,缺了就看不到源码行号或变量名
为什么不用 asmtools 或手写解析器
asmtools(如 jdec)输出的是 JCED 语法,接近字节码规范原文,但可读性差、无注释、不校验结构合法性;而自己用 DataInputStream 读 magic、major_version 等字段,看似底层,实则极易踩坑:
- 常量池里
CONSTANT_Long和CONSTANT_Double占两个索引槽,循环从i = 1开始但遇到它们得跳过i++,漏掉就整个解析偏移错乱 - UTF-8 字符串在常量池中是变长编码,不能简单按字节读,得解析长度前缀再读内容
- OpenJDK 17+ 对 class 文件校验更严,非法的
access_flags或错位的attributes长度会导致 JVM 直接拒绝加载,但javap仍能尽力输出已识别部分
运行时 dump 类的字节码要另走 sa-jdi
如果目标 class 文件根本不存在磁盘上(比如动态代理生成的 $Proxy0、Lambda 生成的类),javap 就无能为力了。这时得用 JDK 的诊断工具链:
- 先用
jps -l找到目标进程 PID - 再用
java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.tools.jcore.ClassDump把运行时类 dump 成.class文件 - 最后才轮到
javap -verbose查看 —— 注意:sa-jdi 需要 JVM 启动时加-XX:+UnlockDiagnosticVMOptions,且不支持容器环境下的普通进程
真正难的从来不是“怎么看到字节码”,而是理解某条 athrow 指令为何没进 catch、或者 invokedynamic 的 bootstrap 方法到底绑定到了哪——这些得结合常量池索引、方法描述符、栈帧状态一起看,javap 只负责把原始材料摆上来,不替你做推理。











