转载

Java 异常表与异常处理原理

最近写代码的时候遇到一些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 代码的语义上讲,执行路径分别为:

  1. 如果 try 语句块中出现了属于 Exception 及其子类的异常,则跳转到 catch 处理;
  2. 如果 try 语句块中出现了不属于 Exception 及其子类的异常,则跳转到 finally 处理;
  3. 如果 catch 语句块中出现了任何异常,则跳转到 finally 处理。

由此可以分析此段代码可能的返回结果:

  1. 如果没有出现异常,返回1;
  2. 如果出现 Exception 异常,返回2;
  3. 如果出现了 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,如果没有发生异常,则之后进行跳转,否则往下执行,即执行 astoreiconstistore ,即保留之前的栈顶位置,对x赋值为4。

最后总结一下,Java通过异常表来捕捉异常,在表中针对发生的异常能够获取接下来执行到哪里(从try跳转到catch),除了指定的异常外,还会自动追加any异常,用来捕获程序中没有捕获的异常。而finally会自动的追加到try、catch以及未捕获到的异常后面执行。对于多层次的 try{}catch{} ,同理。

ps. 最后有一个彩蛋,就是异常表后面会追加一个指向自己start_pc的条目,这里有一些讨论可以看看。

原文  https://juejin.im/post/5c4d5bed6fb9a049c9660e54
正文到此结束
Loading...