异常处理的实现
异常处理的实现
在Java代码中,我们可通过try-catch-finally块对异常进行捕获或处理。其中catch块可以有零个或多个,finally块可有可无。如果catch有多个,而第一个catch的异常的类型是后面catch的异常的类型的父类,那么后面的catch块不会起作用。那么我们如何在字节码层面实现try-catch-finally块呢?
try-catch
我们来看一个简单的try-catch使用例子,如代码清单3-58所示。
代码清单3-58 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代码块的代码总能被执行到?我们来看一个例子,如代码清单3-58所示。
代码清单3-59 try-catch-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语法糖的使用如代码清单3-60所示。
代码清单3-60 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块。
注释:
[1] Oracle官方文档:https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html