最近写代码的时候遇到一些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 代码的语义上讲,执行路径分别为:
由此可以分析此段代码可能的返回结果:
我们来分析字节码:
首先,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的条目,这里有一些讨论可以看看。