异常处理/同步控制指令
异常与同步:字节码的两把「锁」
这一节来看字节码中两个高级主题:异常处理和同步控制。异常处理决定了程序出错时的行为,同步控制则实现了 synchronized 关键字。
异常抛出指令
athrow:抛出异常
athrow // 弹出栈顶引用作为异常抛出// 源码
throw new IllegalArgumentException("参数不能为空");
// 字节码
new #IllegalArgumentException // 创建异常对象
dup // 复制引用
ldc #"参数不能为空" // 字符串常量入栈
invokespecial #init // 构造函数
athrow // 抛出异常异常对象的创建和抛出:创建异常对象 → 复制引用 → 初始化 → 抛出。dup 的原因是 invokespecial 会消耗引用,所以需要先复制一份。
显式异常抛出 vs JVM 自动抛出
// 显式抛出(程序员写 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. 如果没有匹配的处理器,弹出当前栈帧,向调用者传播异常表的执行
// 源码
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
// 源码
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 块的末尾:
// 源码
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 语法糖:
// 源码
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 实例方法
// 源码
public synchronized void method() {
// 同步代码
}字节码:
public synchronized void method();
flags: ACC_SYNCHRONIZED // 方法标志位
Code:
...
monitorexit // (隐式,在方法返回前)实例方法的同步:方法级别的 synchronized 在方法调用前后隐式加锁/解锁。通过 ACC_SYNCHRONIZED 标志实现。
synchronized 代码块
// 源码
public void method() {
synchronized (obj) {
// 同步代码
}
}字节码:
aload_1 // obj 引用入栈(锁对象)
dup // 复制引用(monitorenter 消耗一个)
monitorenter // 获取锁(obj 的 monitor)
// 同步代码
...
monitorexit // 退出同步块,释放锁
aload_x // 加载其他需要的值
areturn // 返回
// 如果同步代码中抛出异常,JVM 自动插入 monitorexitsynchronized 的异常处理
// 源码
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 是可重入锁——同一个线程可以多次获取同一对象的锁:
// 源码
synchronized (obj) {
synchronized (obj) { // 同一个线程可以再次获取
// ...
}
}monitorenter 在获取锁时,会检查当前线程是否已经持有该锁。如果是,锁计数器 +1,monitorexit 时计数器 -1,直到计数器归零才真正释放锁。
完整示例:异常与同步
// 源码
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 一定执行 |
| synchronized | monitorenter/monitorexit | 基于对象 monitor 的锁机制 |
| 方法级同步 | ACC_SYNCHRONIZED | 隐式加锁/解锁整个方法 |
| 可重入 | 锁计数器机制 | 同一线程可多次获取同一锁 |
异常处理的关键:JVM 通过异常表决定抛出异常时的跳转目标,finally 通过在所有退出路径复制代码实现。
到这里,「字节码指令集」部分全部完成。接下来进入「类加载深度剖析」部分,首先看 类生命周期(加载/链接/初始化/使用/卸载)。
