📚《深入浅出JVM字节码》

《Java虚拟机字节码:从入门到实战》的开源版本。作者通过自己的实战经验,整合出一套适合新手的高效学习教程。归纳并提炼知识点,制定合理路线,帮助读者更快掌握核心技术。


异常处理的实现

在Java代码中,我们可通过try-catch-finally块对异常进行捕获或处理。其中catch块可以有零个或多个,finally块可有可无。如果catch有多个,而第一个catch的异常的类型是后面catch的异常的类型的父类,那么后面的catch块不会起作用。那么我们如何在字节码层面实现try-catch-finally块呢?

try-catch

我们来看一个简单的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代码块处理这个异常,那么当前线程将会异常结束,线程被虚拟机销毁。

try-catch-finally

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代码块中的代码都会被执行。

try-with-resource语法糖

在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虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!


  1. Oracle官方文档:https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

📚目录