博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java 异常表与异常处理原理
阅读量:6509 次
发布时间:2019-06-24

本文共 5071 字,大约阅读时间需要 16 分钟。

最近写代码的时候遇到一些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的条目,这里有可以看看。

码字不易,如有建议请扫码

转载地址:http://grbfo.baihongyu.com/

你可能感兴趣的文章
《CCNP TSHOOT 300-135学习指南》——第2章 结构化故障检测与排除进程
查看>>
《Java EE 7精粹》—— 2.5 非阻塞I/O
查看>>
《Python数据科学实践指南》一2.2 字符串
查看>>
《R数据可视化手册》——1.1 安装包
查看>>
《iOS创意程序设计家》——导读
查看>>
spring-aop
查看>>
android RecycleView Adapter简单封装
查看>>
PgSQL · 案例分享 · 递归收敛优化
查看>>
Dart的数据库操作
查看>>
Codeforces 591 B Rebranding【Codeforces Round #327 (Div. 2)】
查看>>
命名难,难于上青天
查看>>
APUE读书笔记-05标准输入输出库(7)
查看>>
23 第一周作业
查看>>
DNS解析偶尔延迟
查看>>
iOS打电话,发短信,发邮件,打开网址
查看>>
06-验证码-基本功能实现
查看>>
Java数据结构与算法(六) 希尔排序
查看>>
canvas学习笔记
查看>>
elasticsearch安装步骤
查看>>
PHP获取Cookie模拟登录CURL(转)
查看>>