Skip to content

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 准备一个综合示例类

java
// 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");
    }
}
bash
javac Demo.java
javap -v Demo.class

3.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

&lt;init&gt;(构造函数): 名称 "&lt;init&gt;" 是 JVM 规定的构造函数固定名称。注意这里编译器为 counter = 0 自动生成的代码就是直接赋零值(在字段表中有 ConstantValue 属性时),或者初始化在构造函数中。字段初始值 = 0 是 int 类型的默认值,其实可以不写——但这里写了,编译器可能会在构造函数中生成赋值指令,class 文件中的显式初始化会出现在构造函数里。

实际上,private int counter = 0; 这种给默认值的情况,现代 javac 优化掉了,因为 int 的默认值就是 0。所以 &lt;init&gt; 里只有 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: 19

sum 方法: 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

&lt;clinit&gt;(类初始化方法): JVM 将静态代码块和静态字段初始化合并到 &lt;clinit&gt; 方法中。JVM 在类的初始化阶段执行它,并保证多线程安全(一个线程执行 &lt;clinit&gt; 时,其他线程阻塞等待)。

四、模式识别技巧

4.1 &lt;init&gt; vs &lt;clinit&gt; 的区别

&lt;init&gt;&lt;clinit&gt;
调用时机对象创建时 (new 指令)类首次被主动引用时
JVM 并发保证无 (多线程同时 new 没问题)有 (只执行一次,其他线程等待)
合并内容所有构造函数代码 + 实例字段初始化静态字段初始化 + 静态代码块
方法命名"&lt;init&gt;""&lt;clinit&gt;"
访问标志ACC_PUBLIC / ACC_PRIVATE 等ACC_STATIC

4.2 synchronized 方法 vs synchronized 块

java
// 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 的区别

java
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) 存入 b

4.4 String 拼接的优化

java
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 文件结构
&lt;init&gt;&lt;clinit&gt; 的区别?&lt;init&gt; 是构造函数,实例化时调用;&lt;clinit&gt; 是类初始化方法,将静态字段赋值和静态代码块合并而成,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 → 多次 appendtoString。但在循环中使用时,每次迭代会 new 一个 StringBuilder,建议显式使用 StringBuilder 避免对象创建开销

📚 相关链接

  • **Class文件结构** — Class 文件的每个字段对应 javap 的哪部分输出
  • **字节码指令集概览** — invoke、monitorenter 等指令的详细语义
  • **ASM与Javassist字节码操作入门** — 用编程方式分析和修改字节码
  • ← 返回 **字节码索引**

Knowledge4J — Java 知识库