Java的try-with-resources是编译期语法糖,由编译器重写为含空值检查、close()调用及suppressed exception处理的try-finally结构,多资源按声明逆序关闭。

Java 的 try-with-resources 是编译期语法糖,底层不依赖 JVM 新指令,而是由编译器重写为等效的 try-finally 结构并插入资源关闭逻辑。分析其字节码,关键在于观察编译后生成的 finally 块、空值检查、close() 调用及异常压制(suppressed exception)处理机制。
编译器如何重写 try-with-resources
以单资源为例:
try (FileInputStream fis = new FileInputStream("a.txt")) {
fis.read();
}
编译后等价于(语义近似,非完全一致):
FileInputStream fis = new FileInputStream("a.txt");
try {
fis.read();
} finally {
if (fis != null) {
try {
fis.close();
} catch (Throwable t2) {
t.addSuppressed(t2);
}
}
}
注意:真实字节码中不会出现 Java 源码级的 if 和嵌套 try,而是通过栈操作和分支指令实现空值判断与异常捕获。
立即学习“Java免费学习笔记(深入)”;
- 编译器为每个资源生成独立的
finally逻辑段,确保即使close()抛异常,也不中断主异常传播 - 资源变量在字节码中被声明为局部变量(如
astore_2),并在finally前用aload_2加载进行非空校验 - 若资源构造本身失败(如
new FileInputStream抛出FileNotFoundException),则该变量仍为null,跳过close()
多资源的关闭顺序与字节码体现
多个资源按声明**逆序**关闭,例如:
try (ResourceA a = new ResourceA();
ResourceB b = new ResourceB()) {
// ...
}
字节码中会先生成 b.close() 的 finally 逻辑,再生成 a.close() 的逻辑 —— 即外层 finally 包裹内层 finally,形成嵌套结构。
- 这种嵌套不是源码级嵌套,而是编译器在字节码中安排的执行路径:先执行后声明资源的
close(),再执行先声明的 - 每个
close()调用都伴随独立的异常捕获块,使用addSuppressed()将后续关闭异常附加到主异常上 - 可通过
javap -c查看具体指令:搜索INVOKEVIRTUAL.*close和IFNULL指令对,即可定位关闭逻辑位置
实操:用 javap 分析字节码
步骤如下:
- 编写含
try-with-resources的类,编译:javac MyClass.java - 反编译查看字节码:
javap -c MyClass - 查找关键模式:
-
ACONST_NULL ASTORE_N→ 初始化资源变量为空 -
NEW ... DUP INVOKESPECIAL→ 资源实例化 -
ASTORE_N→ 存入局部变量表 -
ALOAD_N IFNULL Lxx→ 空值检查跳转 -
ALOAD_N INVOKEVIRTUAL .*close→ 实际关闭调用 -
TRYCATCHBLOCK Lx Ly Lz→ 异常压制所需的异常处理区块
-
你会发现:所有 close() 调用都出现在 finally 对应的字节码标签(如 L2)中,且多个资源对应多个连续的 TRYCATCHBLOCK 条目。
注意事项:Java 9+ 的优化变化
Java 9 支持“已声明资源”的写法:
ResourceA a = new ResourceA();
ResourceB b = new ResourceB();
try (a; b) { ... }
此时字节码中不再对 a 和 b 做 ASTORE/ALOAD 的重复赋值,而是直接复用已有变量槽位,减少栈操作。但关闭顺序规则不变:仍是 b 先于 a 关闭。
- 该写法让资源作用域更灵活,也便于调试时观察变量状态
- 字节码层面,
try (a; b)生成的finally逻辑与显式声明一致,只是省略了构造后的存储指令 - 仍需确保变量为
final或“实际上 final”,否则编译报错


















