📚《深入浅出JVM字节码》
《Java虚拟机字节码:从入门到实战》的开源版本。作者通过自己的实战经验,整合出一套适合新手的高效学习教程。归纳并提炼知识点,制定合理路线,帮助读者更快掌握核心技术。
在Java代码中,我们可通过try-catch-finally块对异常进行捕获或处理。其中catch块可以有零个或多个,finally块可有可无。如果catch有多个,而第一个catch的异常的类型是后面catch的异常的类型的父类,那么后面的catch块不会起作用。那么我们如何在字节码层面实现try-catch-finally块呢?
我们来看一个简单的try-catch使用例子,代码如下。
public int tryCatchDemo() {
try {
int n = 100;
int m = 0;
return n / m;
} catch (ArithmeticException e) {
return -1;
}
}
使用javap命令输出tryCatchDemo方法的字节码以及异常表信息如下。
public int tryCatchDemo();
Code:
stack=2, locals=3, args_size=1
0: bipush 100
2: istore_1
3: iconst_0
4: istore_2
5: iload_1
6: iload_2
7: idiv
8: ireturn
9: astore_1
10: iconst_m1
11: ireturn
Exception table:
from to target type
0 8 9 Class java/lang/ArithmeticException
异常表存储在Code属性中,异常表每项元素的结构见第二章。tryCatchDemo方法的异常表只有一项,该项的from、to、target存储的是方法字节码指令的偏移量,从from到to的字节码对应try代码块中的代码,target指向的字节码指令是catch代码块的开始,type是该catch块捕获的异常。也就是说,在执行偏移量为0到7的字节码指令时,如果抛出类型为ArithmeticException的异常,那么虚拟机将执行偏移量为9开始的字节码指令。
在本例中,如果try代码块中抛出的不是ArithmeticException异常,虚拟机将结束当前方法的执行,将异常往上抛出。如果直到当前线程的第一个方法都没有遇到catch代码块处理这个异常,那么当前线程将会异常结束,线程被虚拟机销毁。
final语意是如何实现的,为什么finally代码块的代码总能被执行到?我们来看一个例子:
public int tryCatchFinalDemo() {
try {
int n = 100;
int m = 0;
return n / m;
} catch (ArithmeticException e) {
return -1;
} finally {
System.out.println("finally");
}
}
使用javap命令输出tryCatchFinalDemo方法的字节码以及异常表信息如下。
public int tryCatchFinalDemo();
Code:
stack=2, locals=5, args_size=1
0: bipush 100
2: istore_1
3: iconst_0
4: istore_2
5: iload_1
6: iload_2
7: idiv
8: istore_3
9: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
12: ldc #4 // String finally
14: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: iload_3
18: ireturn
19: astore_1
20: iconst_m1
21: istore_2
22: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
25: ldc #4 // String finally
27: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: iload_2
31: ireturn
32: astore 4
34: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
37: ldc #4 // String finally
39: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: aload 4
44: athrow
Exception table:
from to target type
0 9 19 Class java/lang/ArithmeticException
0 9 32 any
19 22 32 any
32 34 32 any
先看异常表。异常表的第一项对应tryCatchFinalDemo方法中的catch,当偏移量为0到9(不包括9)的字节码指令在执行过程中抛出异常时,如果异常类型为ArithmeticException则跳转到偏移量为19的字节码指令,也就是执行catch块。但后面的3项又是什么呢?
对照tryCatchFinalDemo方法编译后的字节码指令看。偏移量为0到9的字节码对应try代码块中的Java代码,而19到22对应catch块中的Java代码,32到42的字节码指令对应finally块中的Java代码。偏移量为32的字节码指令是将异常存储到局部变量表索引为4的Slot,这是因为在执行finally块中的代码之前需要将当前异常保存,以便于在执行完finally块中的代码之后,将异常还原到操作数栈的栈顶。抛出异常的字节码指令为athrow,该指令的操作码为0xBF。
根据异常表的理解,编译器为实现finally语意,在异常表中多生成了三个异常项,捕获的类型为any,即不管任何类型的受检异常,都会执行到target处的字节码。
总的理解就是,当try代码块中发生异常时,如果异常类型是ArithmeticException,则跳转到偏移量为19的字节码指令,如果异常类型不是ArithmeticException,则会匹配到异常表的第二项,跳动到偏移量为32的字节码指令,也就是执行finally块的代码。异常表的第三项,如果偏移量为19到22的字节码指令在执行过程中抛出异常,不管任何受检异常都跳转到finally块执行,偏移量为19到22的字节码指令对应catch块的代码。
从这个例子中可以看出,编译器除了为try代码块或者每个catch代码块都添加一个异常项用于捕获任意受检异常跳转到finally代码块执行之外,还把finally代码块的代码复制到try代码块的尾部,以及 catch代码块的尾部。以此确保任何情况下finally代码块中的代码都会被执行。
在JDK1.7之前,为确保访问的资源被关闭,我们需要为资源的访问代码块添加try-finally确保任何情况下资源都能被关闭,但由于关闭资源的close方法也可能抛出异常,因此也需要在finally代码块中嵌套try-catch代码块,这样写出来的代码显得非常的乱。
JDK1.7推出了try-with-resource语法糖1,帮助资源自动释放,不需要在finally块中显示的调用资源的close方法关闭资源,由编译器自动生成。
try-with-resource语法糖使用案例代码如下。
public void tryWithResource() {
try (InputStream in = new FileInputStream("/tmp/xxx.xlsx")) {
// 读取文件
} catch (Exception e) {
}
}
使用javap输出这段代码的字节码如下。
public void tryWithResource();
Code:
stack=3, locals=4, args_size=1
// 创建FileInputStream,局部变量表索引为1的Slot存储FileInputStream对象
0: new #6 // class java/io/FileInputStream
3: dup
4: ldc #7 // String /tmp/xxx.xlsx
6: invokespecial #8 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
9: astore_1
//
10: aconst_null
11: astore_2
12: aload_1
13: ifnull 40
16: aload_2
17: ifnull 36
// 如果局部变量in不为null,且try块抛出的异常不为null,调用close方法
20: aload_1
21: invokevirtual #9 // Method java/io/InputStream.close:()V
24: goto 40
// 调用addSuppressed方法将close方法抛出的异常添加到try代码块抛出的异常
27: astore_3
28: aload_2
29: aload_3
30: invokevirtual #11 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
33: goto 40
// 调用close方法
36: aload_1
37: invokevirtual #9 // Method java/io/InputStream.close:()V
40: goto 44
43: astore_1
44: return
Exception table:
from to target type
20 24 27 Class java/lang/Throwable
0 40 43 Class java/lang/Exception
从tryWithResource方法编译后的字节码可以看出,编译器为try括号内打开的输入流InputStream,在try块的尾部添加了关闭输入流的相关代码。自动添加的字节码指令实现:判断局部变量in是否为空,如果不为空则调用局部变量in的close方法,并且为调用close方法的字节码指令也添加了try-catch块。
发布于:2021 年 08 月 21 日
作者: 吴就业
链接: https://github.com/wujiuye/JVMByteCodeGitBook
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
📚目录
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。