Skip to content

异常处理/同步控制指令

异常与同步:字节码的两把「锁」

这一节来看字节码中两个高级主题:异常处理和同步控制。异常处理决定了程序出错时的行为,同步控制则实现了 synchronized 关键字。

异常抛出指令

athrow:抛出异常

athrow    // 弹出栈顶引用作为异常抛出
java
// 源码
throw new IllegalArgumentException("参数不能为空");

// 字节码
new #IllegalArgumentException    // 创建异常对象
dup                                 // 复制引用
ldc #"参数不能为空"                // 字符串常量入栈
invokespecial #init               // 构造函数
athrow                               // 抛出异常

异常对象的创建和抛出:创建异常对象 → 复制引用 → 初始化 → 抛出。dup 的原因是 invokespecial 会消耗引用,所以需要先复制一份。

显式异常抛出 vs JVM 自动抛出

java
// 显式抛出(程序员写 throw)
throw new NullPointerException();

// 自动抛出(JVM 检测到错误)
String s = null;
int len = s.length();  // JVM 自动抛出 NullPointerException

自动抛出的异常不需要 athrow 指令——JVM 在执行字节码时检测到错误条件,会自动创建并抛出异常对象。

异常表:try-catch 的字节码表示

异常表的结构

回顾 Code 属性的异常表结构:

exception_table {
    u2  start_pc;        // try 块的起始字节码偏移
    u2  end_pc;          // try 块的结束字节码偏移(不包含)
    u2  handler_pc;      // 异常处理器的字节码偏移
    u2  catch_type;      // 捕获的异常类型(0 表示 finally)
}

异常表的查找过程

当方法执行中抛出异常时,JVM 按以下步骤查找异常处理器:

1. 检查异常表,从第一个条目开始
2. 比较抛出位置是否在 [start_pc, end_pc) 范围内
3. 比较异常类型是否匹配(或是其子类)
4. 如果匹配,跳转到 handler_pc
5. 如果不匹配,继续检查下一个条目
6. 如果没有匹配的处理器,弹出当前栈帧,向调用者传播

异常表的执行

java
// 源码
public void readFile() {
    try {
        Files.readString(Path.of("test.txt"));
    } catch (IOException e) {
        System.out.println("读取失败");
    }
}

字节码(关键部分):

# try 块
0:  ldc #"test.txt"
2:  invokestatic #Files.readString
...(try 块中的其他字节码)
n:  nop           // try 块结束

# 跳转到正常退出路径
n+1: goto m

# catch 块(异常处理器)
n+4: astore_1    // 异常引用存入局部变量 e
n+5: getstatic #System.out
n+8: ldc #"读取失败"
n+10: invokevirtual #println
n+13: nop        // catch 块结束

# 正常退出路径
m: ...

对应的异常表:

exception_table_length = 1

exception_table[0]:
  start_pc = 0        // try 块起始
  end_pc = n+1        // try 块结束(不包含)
  handler_pc = n+4   // catch 块起始
  catch_type = IOException  // 捕获 IOException

多重 catch

java
// 源码
try {
    // 可能抛出 IOException 或 RuntimeException 的代码
} catch (IOException e) {
    // 处理 IOException
} catch (RuntimeException e) {
    // 处理 RuntimeException
} catch (Exception e) {
    // 处理其他 Exception
}

字节码中有多个异常表条目,JVM 按顺序查找:

exception_table:
  #1: IOException    → handler: catch_IOException
  #2: RuntimeException → handler: catch_RuntimeException
  #3: Exception     → handler: catch_Exception

注意:异常表条目按源代码顺序排列,JVM 从头开始匹配。

finally 的字节码实现

finally 是字节码中最复杂的结构之一。编译器会把 finally 代码复制到 try 块和每个 catch 块的末尾:

java
// 源码
try {
    return doSomething();
} finally {
    cleanup();
}

字节码结构:

try 块开始
    doSomething() 结果入栈
    dup              // 复制结果(用于 finally)
    astore_0         // 结果存入 slot 0
    跳转到 finally_块
try 块结束

catch (所有异常)
    异常引用入栈
    跳转到 finally_块

finally_块:
    pop              // 弹出异常引用或结果
    invokestatic #cleanup  // 执行 cleanup
    (如果有存储的结果) aload_0; areturn  // 返回原始结果

finally 的本质:编译器把 finally 代码复制到所有退出路径(正常返回和异常抛出)。

try-with-resources

JDK 7 的 try-with-resources 语法糖:

java
// 源码
try (BufferedReader br = new BufferedReader(...)) {
    return br.readLine();
}

编译器生成的字节码包含:

1. 创建资源对象
2. 调用资源.close() 在 finally 中
3. suppressed 异常的处理(如果 close() 也抛异常)

同步控制:synchronized

monitorenter / monitorexit

synchronized 关键字的底层实现:

monitorenter    // 获取对象的 monitor(锁)
monitorexit     // 释放对象的 monitor(锁)

每个 Java 对象都有一个关联的 monitor(监视器锁),monitorenter 尝试获取锁,monitorexit 释放锁。

synchronized 实例方法

java
// 源码
public synchronized void method() {
    // 同步代码
}

字节码:

public synchronized void method();
  flags: ACC_SYNCHRONIZED    // 方法标志位
  Code:
    ...
    monitorexit   // (隐式,在方法返回前)

实例方法的同步:方法级别的 synchronized 在方法调用前后隐式加锁/解锁。通过 ACC_SYNCHRONIZED 标志实现。

synchronized 代码块

java
// 源码
public void method() {
    synchronized (obj) {
        // 同步代码
    }
}

字节码:

aload_1               // obj 引用入栈(锁对象)
dup                   // 复制引用(monitorenter 消耗一个)
monitorenter          // 获取锁(obj 的 monitor)

// 同步代码
...

monitorexit           // 退出同步块,释放锁
aload_x               // 加载其他需要的值
areturn               // 返回

// 如果同步代码中抛出异常,JVM 自动插入 monitorexit

synchronized 的异常处理

java
// 源码
public void method() {
    synchronized (this) {
        doSomething();  // 可能抛出异常
    }
}

字节码中的异常表:

exception_table:
  start_pc = monitorenter 偏移
  end_pc = monitorexit 偏移
  handler_pc = monitorexit 偏移  // 异常处理器
  catch_type = 0               // 捕获所有异常(包括 finally)

doSomething() 抛出异常时,JVM 会跳转到 monitorexit 执行,确保锁被正确释放。

锁的膨胀

HotSpot JVM 中,monitorenter/monitorexit 的实现会经历锁的「膨胀」过程:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
阶段机制适用场景
偏向锁记录线程 ID,后续无需同步无竞争,单线程
轻量级锁CAS 自旋获取短时锁竞争(线程交替执行)
重量级锁OS mutex,线程阻塞长时锁竞争,多线程

synchronized 可重入性

synchronized 是可重入锁——同一个线程可以多次获取同一对象的锁:

java
// 源码
synchronized (obj) {
    synchronized (obj) {  // 同一个线程可以再次获取
        // ...
    }
}

monitorenter 在获取锁时,会检查当前线程是否已经持有该锁。如果是,锁计数器 +1,monitorexit 时计数器 -1,直到计数器归零才真正释放锁。

完整示例:异常与同步

java
// 源码
public class Demo {
    public synchronized void process() {
        try {
            for (int i = 0; i < 10; i++) {
                if (i == 5) {
                    throw new RuntimeException("i=5");
                }
            }
        } finally {
            System.out.println("清理资源");
        }
    }
}

字节码:

#  public synchronized void process();
#    flags: ACC_SYNCHRONIZED
#    Code:
#      stack=2, locals=3, args_size=1
#
#      0: iconst_0          // i = 0
#      1: istore_1
#      2: iload_1           // i 入栈
#      3: bipush          10  // 10 入栈
#      5: if_icmpge 30       // i >= 10 跳出循环
#
#      8: iload_1           // i 入栈
#      9: iconst_5         // 5 入栈
#     10: if_icmpne 27      // i != 5 跳到循环末尾
#
#     13: new #RuntimeException
#     16: dup
#     17: ldc #"i=5"
#     19: invokespecial #RuntimeException.<init>
#     22: athrow             // 抛出异常
#
#     23: astore_2          // 异常存入 slot 2(不常用路径)
#     24: aload_2           // 异常引用入栈
#     25: athrow             // 重新抛出
#
#     26: iinc 1 by 1       // i++(跳过 i==5 时执行不到)
#
#     27: iinc 1 by 1       // i++(正常循环)
#     30: goto 2            // 跳回循环
#
#     33: getstatic #System.out
#     36: ldc #"清理资源"
#     38: invokevirtual #println
#     39: return
#
#    Exception table:
#      from    to  target  type
#        0     22     23   any

本节小结

异常与同步核心要点:

类别指令说明
抛出异常athrow弹出栈顶引用作为异常抛出
异常表Code 属性的一部分描述 try-catch-finally 的跳转逻辑
查找顺序按源码顺序匹配第一个匹配的处理器生效
finally代码复制到所有退出路径确保 cleanup 一定执行
synchronizedmonitorenter/monitorexit基于对象 monitor 的锁机制
方法级同步ACC_SYNCHRONIZED隐式加锁/解锁整个方法
可重入锁计数器机制同一线程可多次获取同一锁

异常处理的关键:JVM 通过异常表决定抛出异常时的跳转目标,finally 通过在所有退出路径复制代码实现。

到这里,「字节码指令集」部分全部完成。接下来进入「类加载深度剖析」部分,首先看 类生命周期(加载/链接/初始化/使用/卸载)

基于 VitePress 构建