最近写代码的时候遇到一些try catch的问题。
try {
代码块1
} catch (Exception e) {
代码块2
} finally {
代码块3
}
复制代码
在代码块1执行的时候发生异常,但是代码块2没有执行,代码块3执行了,排查半天发现代码块1中抛出的并不是Exception及其子类。那么没有catch住的try catch流程到底是怎么样的呢?
之前也简单看过一些jvm try catch原理,这里尝试记录总结一下。
Java 在代码中通过使用 try{}catch(){}finally{}
块来对异常进行捕获或者处理。但是对于 JVM 来说,是如何处理 try/catch 代码块与异常的呢。
实际上 Java 编译后,会在代码后附加异常表的形式来实现 Java 的异常处理及 finally 机制(在 JDK1.4.2之前,javac 编译器使用 jsr 和 ret 指令来实现 finally 语句,但是1.4.2之后自动在每段可能的分支路径后将 finally 语句块内容冗余生成一遍来实现。JDK1.7及之后版本,则完全禁止在 Class 文件中使用 jsr 和 ret 指令)。
异常表
属性表(attribute_info)可以存在于 Class 文件、字段表、方法表中,用于描述某些场景的专有信息。属性表中有个 Code 属性,该属性在方法表中使用,Java 程序方法体中的代码被编译成的字节码指令存储在 Code 属性中。而异常表(exception_table)则是存储在 Code 属性表中的一个结构,这个结构是可选的。
异常表结构
异常表结构如下表所示。它包含四个字段:如果当字节码在第 start_pc 行到 end_pc 行之间(即[start_pc, end_pc))出现了类型为 catch_type 或者其子类的异常(catch_type 为指向一个 CONSTANT_Class_info 型常量的索引),则跳转到第 handler_pc 行执行。如果 catch_type 为0,表示任意异常情况都需要转到 handler_pc 处进行处理。
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
处理异常机制
如上面所说,每个类编译后,都会跟随一个异常表,如果发生异常,首先在异常表中查找对应的行(即代码中相应的 try{}catch(){}
代码块),如果找到,则跳转到异常处理代码执行,如果没有找到,则返回(执行 finally 之后),并 copy 异常的应用给父调用者,接着查询父调用的异常表,以此类推。
异常处理实例
对于 Java 源码:
public class Test {
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
复制代码
将其编译为 ByteCode 字节码(JDK版本1.8):
public int inc();
Code:
0: iconst_1 #try中x=1入栈
1: istore_1 #x=1存入第二个int变量
2: iload_1 #将第二个int变量推到栈顶
3: istore_2 #将栈顶元素存入第三个变量,即保存try中的返回值
4: iconst_3 #final中的x=3入栈
5: istore_1 #栈顶元素放入第二个int变量,即final中的x=3
6: iload_2 #将第三个int变量推到栈顶,即try中的返回值
7: ireturn #当前方法返回int,即x=1
8: astore_2 #栈顶数值放入当前frame的局部变量数组中第三个
9: iconst_2 #catch中的x=2入栈
10: istore_1 #x=2放入第二个int变量
11: iload_1 #将第二个int变量推到栈顶
12: istore_3 #将栈顶元素存入第四个变量,即保存catch中的返回值
13: iconst_3 #final中的x=3入栈
14: istore_1 #final中的x=3放入第一个int变量
15: iload_3 #将第四个int变量推到栈顶,即保存的catch中的返回值
16: ireturn #当前方法返回int,即x=2
17: astore 4 #栈顶数值放入当前frame的局部变量数组中第五个
19: iconst_3 #final中的x=3入栈
20: istore_1 #final中的x=3放入第一个int变量
21: aload 4 #当前frame的局部变量数组中第五个放入栈顶
23: athrow #将栈顶的数值作为异常或错误抛出
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
复制代码
首先可以看到,对于 finally,编译器将每个可能出现的分支后都放置了冗余。并且编译器生成了三个异常表记录,从 Java 代码的语义上讲,执行路径分别为:
- 如果 try 语句块中出现了属于 Exception 及其子类的异常,则跳转到 catch 处理;
- 如果 try 语句块中出现了不属于 Exception 及其子类的异常,则跳转到 finally 处理;
- 如果 catch 语句块中出现了任何异常,则跳转到 finally 处理。
由此可以分析此段代码可能的返回结果:
- 如果没有出现异常,返回1;
- 如果出现 Exception 异常,返回2;
- 如果出现了 Exception 意外的异常,非正常退出,没有返回;
我们来分析字节码:
首先,0-3行,就是把整数1赋值给 x,并且将此时 x 的值复制一个副本到本地变量表的 Slot 中暂存,这个 Slot 里面的值在 ireturn 指令执行前会被重新读到栈顶,作为返回值。这时如果没有异常,则执行4-5行,把 x 赋值为3,然后返回前面保存的1,方法结束。如果出现异常,读取异常表发现应该执行第8行,pc 寄存器指针转向8行,8-16行就是把2赋值给 x,然后把 x 暂存起来,再将 x 赋值为3,然后将暂存的2读到操作栈顶返回。第17行开始是把 x 赋值为3并且将栈顶的异常抛出,方法结束。
上面是一个比较简单的Java程序,这里稍微复杂化它,尝试在finally中增加异常模块:
public class Test {
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
try{
x = 3;
} catch (Exception e) {
x = 4;
}
}
}
}
复制代码
将其编译为 ByteCode 字节码:
public int inc();
Code:
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: goto 12
9: astore_3
10: iconst_4
11: istore_1
12: iload_2
13: ireturn
14: astore_2
15: iconst_2
16: istore_1
17: iload_1
18: istore_3
19: iconst_3
20: istore_1
21: goto 28
24: astore 4
26: iconst_4
27: istore_1
28: iload_3
29: ireturn
30: astore 5
32: iconst_3
33: istore_1
34: goto 41
37: astore 6
39: iconst_4
40: istore_1
41: aload 5
43: athrow
Exception table:
from to target type
4 6 9 Class java/lang/Exception
0 4 14 Class java/lang/Exception
19 21 24 Class java/lang/Exception
0 4 30 any
14 19 30 any
32 34 37 Class java/lang/Exception
30 32 30 any
复制代码
和上面一样,0-3行为try内语句,保存x=1并准备返回,如果发生异常则查询异常表,跳转执行14行;14-18行为catch部分语句,保存x=2并准备返回;4-6行、19-21行、32-34行为finally中语句,首先设置x=3,如果没有发生异常,则之后进行跳转,否则往下执行,即执行astore
,iconst
,istore
,即保留之前的栈顶位置,对x赋值为4。
最后总结一下,Java通过异常表来捕捉异常,在表中针对发生的异常能够获取接下来执行到哪里(从try跳转到catch),除了指定的异常外,还会自动追加any异常,用来捕获程序中没有捕获的异常。而finally会自动的追加到try、catch以及未捕获到的异常后面执行。对于多层次的try{}catch{}
,同理。
ps. 最后有一个彩蛋,就是异常表后面会追加一个指向自己start_pc的条目,这里有一些讨论可以看看。
码字不易,如有建议请扫码