异常处理
本文从字节码角度出发,分析Java异常处理机制。
try-finally
package 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 #2 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #3 <try>
5 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// 正常执行
// finally 部分
8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
11 ldc #5 <finally>
13 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// 正常退出
16 goto 30 (+14)
// 异常跳转到此处
// finally 部分
19 astore_1
20 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
23 ldc #5 <finally>
25 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
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
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.one.exception.TryCatchFinally.main(TryCatchFinally.java:6)
*/
通过测试可以知道尽管我们没有捕捉到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 #2 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #3 <try>
5 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// finally 代码块
8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
11 ldc #5 <finally>
13 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// 正常退出
16 goto 50 (+34)
// 异常捕获1
19 astore_1
// catch 代码块
20 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
23 ldc #7 <catch>
25 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// finnaly 代码块
28 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
31 ldc #5 <finally>
33 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// 捕获成功后正常退出
36 goto 50 (+14)
// 异常捕获2
39 astore_2
// finally 代码块
40 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
43 ldc #5 <finally>
45 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// 向外抛出异常
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 #2 <java/lang/System.out : Ljava/io/PrintStream;>
43 ldc #5 <finally>
45 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// 向外抛出异常
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 #2 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #3 <try>
5 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
//主动抛出异常
8 new #5 <java/lang/RuntimeException>
11 dup
12 invokespecial #6 <java/lang/RuntimeException.<init> : ()V>
15 athrow
16 astore_1
17 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
20 ldc #8 <catch>
22 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
25 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
28 ldc #9 <finally>
30 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
33 goto 47 (+14)
36 astore_2
37 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
40 ldc #9 <finally>
42 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
45 aload_2
46 athrow
47 return
athrow
大概就是会获取一个Throwable的异常对象,如果是NULL就抛出空指针异常,并且如果涉及到同步块,会处理monitor
finally-with-resource
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 #2 <com/one/exception/TryWithResource>
3 dup
4 invokespecial #3 <com/one/exception/TryWithResource.<init> : ()V>
7 astore_1
8 aconst_null
9 astore_2
10 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
13 ldc #5 <try>
15 invokevirtual #6 <java/io/PrintStream.println : (Ljava/lang/String;)V>
18 aload_1
19 ifnull 89 (+70)
22 aload_2
23 ifnull 42 (+19)
26 aload_1
27 invokevirtual #7 <com/one/exception/TryWithResource.close : ()V>
30 goto 89 (+59)
33 astore_3
34 aload_2
35 aload_3
36 invokevirtual #9 <java/lang/Throwable.addSuppressed : (Ljava/lang/Throwable;)V>
39 goto 89 (+50)
42 aload_1
43 invokevirtual #7 <com/one/exception/TryWithResource.close : ()V>
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 <com/one/exception/TryWithResource.close : ()V>
68 goto 86 (+18)
71 astore 5
73 aload_2
74 aload 5
76 invokevirtual #9 <java/lang/Throwable.addSuppressed : (Ljava/lang/Throwable;)V>
79 goto 86 (+7)
82 aload_1
83 invokevirtual #7 <com/one/exception/TryWithResource.close : ()V>
86 aload 4
88 athrow
89 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
92 ldc #10 <finally>
94 invokevirtual #6 <java/io/PrintStream.println : (Ljava/lang/String;)V>
97 goto 133 (+36)
100 astore_1
101 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
104 ldc #12 <catch>
106 invokevirtual #6 <java/io/PrintStream.println : (Ljava/lang/String;)V>
109 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
112 ldc #10 <finally>
114 invokevirtual #6 <java/io/PrintStream.println : (Ljava/lang/String;)V>
117 goto 133 (+16)
120 astore 6
122 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
125 ldc #10 <finally>
127 invokevirtual #6 <java/io/PrintStream.println : (Ljava/lang/String;)V>
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
简化资源对象的使用