01.3 - 实战 javap 反汇编分析
定位: JDK 自带的反汇编工具——从 Class 文件还原为人类可读的结构 面试高频度: ⭐⭐⭐⭐ 考查方式: 给一段 javap 输出要求分析其源码结构(构造器 vs 静态块、synchronized 块、try-catch 异常表)
一、javap 是什么?
javap 是 JDK 内置的 Class 文件反汇编器。它读取 .class 文件并输出其结构化内容:
- 所有 JVM 安装自带的零依赖工具
- 不需要源代码即可查看类的内部结构
- 是学习 Class 文件格式和字节码的最佳实践工具
二、核心参数
| 参数 | 作用 | 使用场景 |
|---|---|---|
-c | 显示字节码指令 | 快速查看方法的实际指令 |
-v (verbose) | 完整输出:常量池、访问标志、属性表、行号表等 | 深入学习 Class 文件结构 |
-p | 包含私有成员(默认只显示 public/protected) | 查看所有字段和方法 |
-s | 显示字段/方法描述符(类型签名) | 理解 JVM 的类型编码 |
-l | 显示行号表和局部变量表 | 调试信息分析 |
-verbose | 同 -v | — |
三、实战案例
3.1 准备一个综合示例类
// Demo.java
public class Demo {
private int counter = 0;
public Demo() {}
public int sum(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += i;
}
return result;
}
public String handle(String input) {
synchronized (this) {
try {
String s = input.trim();
return s;
} catch (Exception e) {
return "error";
} finally {
System.out.println("cleanup");
}
}
}
static {
System.out.println("class loaded");
}
}javac Demo.java
javap -v Demo.class3.2 完整输出逐区域标注
Classfile /tmp/Demo.class
Last modified 2020-01-01; size 1024 bytes
MD5 checksum ...
Compiled from "Demo.java"SourceFile 属性: 记录原始源文件名,编译后保留了这个信息。
public class Demo
minor version: 0
major version: 52 ← JDK 8 编译 (major = 52)
flags: ACC_PUBLIC, ACC_SUPER ← 公开类 + 使用增强 invokepecial 语义版本号 + 访问标志: 类的基本身份信息。
Constant pool:
#1 = Methodref #8.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #7.#19 // Demo.counter:I
#3 = Class #20 // java/lang/System
#4 = Fieldref #3.#21 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #22 // "class loaded"
#6 = Methodref #23.#24 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #25 // Demo
#8 = Class #26 // java/lang/Object
#9 = Utf8 "counter"
#10 = Utf8 "I"
#11 = Utf8 "<init>"
#12 = Utf8 "()V"
#13 = Utf8 "Code"
#14 = Utf8 "LineNumberTable"
#15 = Utf8 "sum"
#16 = Utf8 "(I)I"
#17 = Utf8 "handle"
#18 = NameAndType #11:#12 // "<init>":()V
#19 = NameAndType #9:#10 // "counter":I
#20 = Utf8 "java/lang/System"
#21 = NameAndType #27:#28 // "out":Ljava/io/PrintStream;
#22 = Utf8 "class loaded"
#23 = Class #29 // java/io/PrintStream
#24 = NameAndType #30:#31 // "println":(Ljava/lang/String;)V
#25 = Utf8 "Demo"
#26 = Utf8 "java/lang/Object"
#27 = Utf8 "out"
#28 = Utf8 "Ljava/io/PrintStream;"
#29 = Utf8 "java/io/PrintStream"
#30 = Utf8 "println"
#31 = Utf8 "(Ljava/lang/String;)V"
#32 = Utf8 "Exception"
...常量池: 编译时生成的静态模板。条目类型包括 Methodref、Fieldref、Class、String、Utf8 等,字节码指令通过
#N引用它们。
{
private int counter;
descriptor: I ← int 类型的字段描述符
flags: ACC_PRIVATE
public Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
<init>(构造函数): 名称"<init>"是 JVM 规定的构造函数固定名称。注意这里编译器为counter = 0自动生成的代码就是直接赋零值(在字段表中有 ConstantValue 属性时),或者初始化在构造函数中。字段初始值= 0是 int 类型的默认值,其实可以不写——但这里写了,编译器可能会在构造函数中生成赋值指令,class 文件中的显式初始化会出现在构造函数里。
实际上,private int counter = 0; 这种给默认值的情况,现代 javac 优化掉了,因为 int 的默认值就是 0。所以 <init> 里只有 aload_0; invokespecial #1; return。但如果是 private int counter = 42;,就会出现 aload_0; ldc 42; putfield counter。
public int sum(int);
descriptor: (I)I ← 参数 int,返回值 int
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: iconst_0
1: istore_2 // result = 0
2: iconst_0
3: istore_3 // i = 0
4: iload_3 // 加载 i
5: iload_1 // 加载 n
6: if_icmpge 15 // if i >= n, goto 15 (方法结束)
9: iload_2 // 加载 result
10: iload_3 // 加载 i
11: iadd // result + i
12: istore_2 // 存回 result
13: iinc 3, 1 // i++
16: goto 4 // 跳回循环判断
19: iload_2 // 加载 result
20: ireturn // 返回 int
LineNumberTable:
line 9: 0
line 10: 2
line 11: 9
line 10: 13
line 13: 19sum 方法: for 循环的经典字节码模式——初始化 → 条件判断 → 循环体 → 递增 → goto 回到判断。
public java.lang.String handle(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=2
0: aload_0 // 加载 this
1: dup // 复制 this 引用 (monitorenter 会消耗栈顶)
2: astore_2 // 将 this 存入 slot 2 (用于 monitorexit)
3: monitorenter ← ⚠️ synchronized 块入口
4: aload_1 // 加载参数 input
5: invokevirtual #2 // Method java/lang/String.trim:()
8: astore_3 // s = input.trim()
9: aload_3 // 加载 s
10: aload_0 // 加载 this (为 monitorexit 做准备)
11: monitorexit ← 正常退出 synchronized 块
12: areturn ← 返回 s (注意:在 finally 之前)关于 synchronized + try-catch-finally 的复合模式: javac 对每一层语义都会生成对应的字节码和保护逻辑。synchronized 块外围有 monitorenter/monitorexit 保证锁获取与释放,内部 try-catch 由异常表控制,finally 则通过"代码复制 + catch-all 异常表"实现在所有路径上都执行。完整的 handle 方法会包含多段异常表条目和栈帧映射,这里展示的是核心跳转逻辑。
Exception table:
from to target type
4 12 19 Class java/lang/Exception ← catch
4 12 31 any ← finally (catch-all)
19 28 31 any ← catch 内的 finally (catch-all)
31 38 31 any ← finally 本身的 catch-all异常表: 核心机制——当 from 到 to 之间的指令抛出指定类型的异常时,跳转到 target 处理。
- 第一行: 4-12 行抛 Exception → 跳 19 行 catch 块
- 第二行: 4-12 行抛任何异常 → 跳 31 行 finally 块 (catch-all)
- finally 块的实现方式就是"代码复制 + 异常表 catch-all"
LineNumberTable:
line 17: 0
line 19: 4
line 20: 9
line 22: 19
line 24: 31
StackMapTable: ...StackMapTable: JDK 6+ 引入,用于加速字节码验证。记录了每个跳转目标的栈状态(栈深度、各位置的类型),这样验证阶段不用逐条指令推导类型。
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out
3: ldc #5 // String "class loaded"
5: invokevirtual #6 // Method java/io/PrintStream.println
8: return
LineNumberTable:
line 28: 0
<clinit>(类初始化方法): JVM 将静态代码块和静态字段初始化合并到<clinit>方法中。JVM 在类的初始化阶段执行它,并保证多线程安全(一个线程执行<clinit>时,其他线程阻塞等待)。
四、模式识别技巧
4.1 <init> vs <clinit> 的区别
<init> | <clinit> | |
|---|---|---|
| 调用时机 | 对象创建时 (new 指令) | 类首次被主动引用时 |
| JVM 并发保证 | 无 (多线程同时 new 没问题) | 有 (只执行一次,其他线程等待) |
| 合并内容 | 所有构造函数代码 + 实例字段初始化 | 静态字段初始化 + 静态代码块 |
| 方法命名 | "<init>" | "<clinit>" |
| 访问标志 | ACC_PUBLIC / ACC_PRIVATE 等 | ACC_STATIC |
4.2 synchronized 方法 vs synchronized 块
// synchronized 方法: 方法上加了 ACC_SYNCHRONIZED 标志
public synchronized void foo() { ... }
// 字节码: flags: ACC_PUBLIC, ACC_SYNCHRONIZED
// 内部: 无 monitorenter/monitorexit,JVM 在方法进入/退出时自动处理
// synchronized 块: 显式 monitorenter/monitorexit
public void bar() {
synchronized (this) { ... }
}
// 字节码: 出现 monitorenter / monitorexit 指令4.3 i++ vs ++i 的区别
public void test() {
int i = 0;
int a = i++; // a = 0, i = 1
int b = ++i; // i = 2, b = 2
}// a = i++:
iload_1 // 加载 i (此时 i=0)
iinc 1, 1 // 局部变量 i 递增 (i=1)
istore_2 // 将原值 (0) 存入 a
// b = ++i:
iinc 1, 1 // 局部变量 i 递增 (i=2)
iload_1 // 加载 i (此时 i=2)
istore_3 // 将新值 (2) 存入 b4.4 String 拼接的优化
public String concat(String a, String b) {
return a + b;
}JDK 5+ 编译为:
new #2 // class java/lang/StringBuilder
dup
invokespecial #3 // Method StringBuilder."<init>":()V
aload_1
invokevirtual #4 // Method StringBuilder.append
aload_2
invokevirtual #4 // Method StringBuilder.append
invokevirtual #5 // Method StringBuilder.toString
areturn为什么用 StringBuilder 而不是 StringBuffer? StringBuilder 是非线程安全的,但方法内的局部变量没有线程安全问题,性能更好。
五、面试视角
| 追问 | 答案要点 |
|---|---|
| javap -v 能看到什么内容? | 魔数+版本号、常量池全部条目、访问标志、字段表、方法表、字节码指令、行号表、异常表、StackMapTable、内部类、引导方法等所有 Class 文件结构 |
<init> 和 <clinit> 的区别? | <init> 是构造函数,实例化时调用;<clinit> 是类初始化方法,将静态字段赋值和静态代码块合并而成,JVM 保证线程安全只执行一次 |
| synchronized 方法和 synchronized 块在字节码上有什么区别? | 方法在 access_flags 上加 ACC_SYNCHRONIZED(无显式指令),块使用 monitorenter / monitorexit 指令包围,并配合异常表保证锁一定释放 |
| 异常表的作用是什么? | 记录 try-catch-finally 的起止偏移量映射。当 from 到 to 之间抛指定类型异常时跳转到 target。finally 块会复制代码 + 用 catch-all (any) 类型的异常表捕获所有未处理异常 |
String s = a + b + c; 在字节码中如何实现? | 编译为 new StringBuilder → 多次 append → toString。但在循环中使用时,每次迭代会 new 一个 StringBuilder,建议显式使用 StringBuilder 避免对象创建开销 |
📚 相关链接
- **Class文件结构** — Class 文件的每个字段对应 javap 的哪部分输出
- **字节码指令集概览** — invoke、monitorenter 等指令的详细语义
- **ASM与Javassist字节码操作入门** — 用编程方式分析和修改字节码
- ← 返回 **字节码索引**