本文从字节码角度出发,分析Java异常处理机制。
try-finallypackage com.one.exception;
public class TryCatchFinally {
public static void main(String[] args) {
try {
System.out.println("try");
}finally {
System.out.println("finally");
}
}
}
// try 部分 0 getstatic #23 ldc #3 5 invokevirtual #4 // 正常执行 // finally 部分 8 getstatic #2 11 ldc #5 13 invokevirtual #4 // 正常退出 16 goto 30 (+14) // 异常跳转到此处 // finally 部分 19 astore_1 20 getstatic #2 23 ldc #5 25 invokevirtual #4 28 aload_1 29 athrow 30 return
try代码块会记录在异常表中,可以看到上述代码中无论是否报错都会执行finally代码块,编译器为正常、异常的情况都编译了finally代码块,通过比较可以发现多了astore_1、aload_1、athrow等指令,在下文中做出解释。
package com.one.exception;
public class TryCatchFinally {
public static void main(String[] args) {
try {
System.out.println("try");
int a = 1/0;
}finally {
System.out.println("finally");
}
}
}
通过测试可以知道尽管我们没有捕捉到try代码块中的异常,但程序仍然执行了finally代码块。
这么设计是为了开发人员可以在finally中进行资源释放,如数据库连接、锁资源、网络连接等,避免产生死锁。
try-catch-finally为测试代码加入catch代码块
package com.one.exception;
public class TryCatchFinally2 {
public static void main(String[] args) {
try {
System.out.println("try");
}catch (Exception e){
System.out.println("catch");
}
finally {
System.out.println("finally");
}
}
}
// try 代码块 0 getstatic #23 ldc #3 5 invokevirtual #4 // finally 代码块 8 getstatic #2 11 ldc #5 13 invokevirtual #4 // 正常退出 16 goto 50 (+34) // 异常捕获1 19 astore_1 // catch 代码块 20 getstatic #2 23 ldc #7 25 invokevirtual #4 // finnaly 代码块 28 getstatic #2 31 ldc #5 33 invokevirtual #4 // 捕获成功后正常退出 36 goto 50 (+14) // 异常捕获2 39 astore_2 // finally 代码块 40 getstatic #2 43 ldc #5 45 invokevirtual #4 // 向外抛出异常 48 aload_2 49 athrow 50 return
首先对比前两个异常处理的相同之处和不同之处
- 起始PC相同(try 代码块开始)
- 结束PC相同(try 代码块结束)
- 跳转PC不同,第一个异常处理跳转到我们定义的catch代码块,第二个异常处理跳转到PC = 39
- 捕获类型不同,一个异常处理捕获Exception,第二个异常处理捕Any
由此可以知道如果在try代码块中发生了Exception类型的异常,则跳转到我们定义的异常捕获中,如果在try代码块中发生了非Exception类型的异常,则跳转到PC = 39
再看第三个异常处理
- 起始PC(catch 代码块开始)
- 结束PC(catch 代码块结束)
- 跳转PC = 39
表示如果在catch代码块中出现了异常,则跳转到PC = 39 处
最后来看PC = 39 到底做了什么?
39 astore_2 // finally 代码块 40 getstatic #243 ldc #5 45 invokevirtual #4 // 向外抛出异常 48 aload_2 49 athrow
很明显运行了finally的代码块,并且通过athrow向外抛出了异常。
throw在try代码块中加入一句throw
package com.one.exception;
public class TryCatchFinally3 {
public static void main(String[] args) throws Exception {
try {
System.out.println("try");
throw new Exception();
}catch (Exception e){
System.out.println("catch");
}
finally {
System.out.println("finally");
}
}
}
0 getstatic #2athrow3 ldc #3 5 invokevirtual #4 //主动抛出异常 8 new #5 11 dup 12 invokespecial #6 : ()V> 15 athrow 16 astore_1 17 getstatic #2 20 ldc #8 22 invokevirtual #4 25 getstatic #2 28 ldc #9 30 invokevirtual #4 33 goto 47 (+14) 36 astore_2 37 getstatic #2 40 ldc #9 42 invokevirtual #4 45 aload_2 46 athrow 47 return
JVM 官方解释
大概就是会获取一个Throwable的异常对象,如果是NULL就抛出空指针异常,并且如果涉及到同步块,会处理monitor
package com.one.exception;
import java.io.Closeable;
import java.io.IOException;
import java.util.Scanner;
public class TryWithResource implements Closeable {
public static void main(String[] args) {
try(TryWithResource tryWithResource = new TryWithResource()){
System.out.println("try");
}catch (Exception e){
System.out.println("catch");
}finally {
System.out.println("finally");
}
}
@Override
public void close() throws IOException {
System.out.println("close");
}
}
0 new #23 dup 4 invokespecial #3 : ()V> 7 astore_1 8 aconst_null 9 astore_2 10 getstatic #4 13 ldc #5 15 invokevirtual #6 18 aload_1 19 ifnull 89 (+70) 22 aload_2 23 ifnull 42 (+19) 26 aload_1 27 invokevirtual #7 30 goto 89 (+59) 33 astore_3 34 aload_2 35 aload_3 36 invokevirtual #9 39 goto 89 (+50) 42 aload_1 43 invokevirtual #7 46 goto 89 (+43) 49 astore_3 50 aload_3 51 astore_2 52 aload_3 53 athrow 54 astore 4 56 aload_1 57 ifnull 86 (+29) 60 aload_2 61 ifnull 82 (+21) 64 aload_1 65 invokevirtual #7 68 goto 86 (+18) 71 astore 5 73 aload_2 74 aload 5 76 invokevirtual #9 79 goto 86 (+7) 82 aload_1 83 invokevirtual #7 86 aload 4 88 athrow 89 getstatic #4 92 ldc #10 94 invokevirtual #6 97 goto 133 (+36) 100 astore_1 101 getstatic #4 104 ldc #12 106 invokevirtual #6 109 getstatic #4 112 ldc #10 114 invokevirtual #6 117 goto 133 (+16) 120 astore 6 122 getstatic #4 125 ldc #10 127 invokevirtual #6 130 aload 6 132 athrow 133 return
指令变得复杂,异常表也变得复杂,但可以清楚关于try-with-resource的几个特点:
- 是 new 一个对象,而不是使用已有得对象(适用于临时打开的资源)
- 编译器在编译过程中自动加入了close的调用,所以不需要手动调用
- 可以实现自动控制多个Closeable接口的资源对象的close
- 遇到异常在异常表中按顺序匹配异常
- 编译器在异常表中隐式设置异常捕获cp_info #0,捕获try语句块、catch语句块,并通过athrow抛出
- try代码块、catch代码块出现异常时,在最后一定会执行finally代码块
- finally代码块中出现异常,只能向外抛出
- 可以使用try-with-resource简化资源对象的使用



