javap:钻进 .class 看字节码
更新时间:2026-09-20。
javap不是反编译回 Java 源码,而是把 class 的结构和字节码打印出来。当你想确认「泛型到底擦没擦」「i++ 和 ++i 差在哪」「synchronized 在字节码层长什么样」时,它是最直接的官方工具。编译基础见 javac/java。
一、基本用法
javac Demo.java # 先要有 .class
javap Demo # 只打印 public/protected 成员签名
javap -p Demo # -p 连 private 成员也显示
javap -c Demo # -c 反汇编每个方法的字节码指令
javap -v Demo # -v 最详细:常量池、栈帧、注解、行号表
javap -classpath out com.demo.Demo # 指定 classpath 与全限定名| 选项 | 作用 |
|---|---|
| 无 | public 方法/字段签名 |
-p | 加上 private 成员 |
-c | 打印字节码指令(最常用) |
-v | 常量池 + 方法表 + 栈帧映射(verify 细节) |
-s | 打印内部类型描述符(Ljava/lang/String;) |
-l | 打印行号与局部变量表 |
二、例 1:i++ 与 ++i 到底差在哪
源码 return i++; 反汇编能看到先 iload(把旧值压栈)再 iinc(自增),返回的是旧值;return ++i; 则先 iinc 后 iload。这正好解释 volatile 为什么救不了 i++:读和写是两条独立指令,中间能插入别的线程。
三、例 2:泛型擦除
编译 List<String> 后 javap -s 看方法描述符,参数类型是 Ljava/util/List;,元素取出处多出一条 checkcast java/lang/String。说明泛型只存在于编译期,运行期对象不带类型参数,与 Java 泛型与类型擦除 一致。
四、例 3:synchronized 与字段标志位
- 同步方法在
javap -v的方法表里带ACC_SYNCHRONIZED标志;同步代码块则出现monitorenter/monitorexit(且编译器插入异常时的隐式 monitorexit); volatile字段带ACC_VOLATILE标志;final字段带ACC_FINAL。这些标志就是 Java 内存模型 在 class 层的落地。
五、例 4:String 拼接与 invokedynamic
JDK 9+ 编译 a + b 不再是 new StringBuilder,而是 invokedynamic makeConcatWithConstants;lambda 也编译成 invokedynamic + 合成方法。javap -c 能直接看到调用指令是 invokevirtual/invokestatic/invokespecial/invokeinterface/invokedynamic 中的哪一种,是理解分派、默认方法与字符串拼接的抓手。
六、怎么读输出(最小心智模型)
JVM 是基于操作数栈的机器:load 类指令把局部变量压栈,add/iinc 等在栈上运算,return/store 弹出。看不懂每条助记符没关系,抓住「压栈→运算→弹栈」的节奏和关键指令(new/dup/invokespecial:<init> 是对象构造,checkcast 是转型,getfield/putfield 是字段读写)即可。真正的反编译(想还原可读源码)用 IDE 或 CFR/Procyon,但要权威结论仍以 javap 为准。
总结
javap 是 JDK 自带的 class 反汇编器:无参看 public 签名、-p 带私有、-c 看字节码指令、-v 看常量池与栈帧注解;用它能直接验证 i++ 先 load 后 inc 的返回旧值、泛型在描述符里被擦除只剩 checkcast、同步方法的 ACC_SYNCHRONIZED 与代码块的 monitorenter/monitorexit、以及 JDK9+ 字符串拼接的 invokedynamic;读输出抓住操作数栈「压栈-运算-弹栈」即可,它给权威字节码结论,还原可读源码才需要 CFR 这类反编译器。