Java字节码结构剖析五:异常表

大家熟悉方法表的Code属性后,知道了Code属性里把字节码指令描述完后,是这个方法的显示异常处理表(下文简称异常表)集合(exception_table[]),异常表对于Code属性来说并不是必须存在的,在Java字节码结构剖析四:属性表里的举例代码里就没有异常表生成,其中Code属性里的exception_table_length为0。

认识异常表

异常表的结构如下:它包含4个字段,这些字段的含义为:如果当字节码在第start_pc行到第end_pc行之间(不包含end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理

exception_table {
    u2 start_pc;
    u2 end_pc;
    u2 handler_pc;
    u2 catch_type;
}

我们来用一段代码来解释,如下所示,一个try-catch-finally的代码,我们从字节码当中去看看jvm是如何使用异常表的。在阅读字节码之前,大家不妨先看看下面的源码,想一下这段代码的返回值在出现异常和不出现异常的情况分别是多少?

public int inc() {
    int x;
    try {
        x = 1;
        return x;
    } catch(Exception e) {
        x = 2;
        return x;
    } finally {
        x = 3;
    }
}

给出上面Java源码编译后的字节码及异常表信息:(大家通过jclasslib bytecode viewer,可以很轻松查看其字节码信息)

Code:
 Stack=1, Locals=5, Args_size=1
 0 iconst_1 
 1 istore_1
 2 iload_1 
 3 istore_2
 4 iconst_3
 5 istore_1  
 6 iload_2
 7 ireturn 
 8 astore_2
 9 iconst_2
10 istore_1
11 iload_1 
12 istore_3
13 iconst_3
14 istore_1
15 iload_3
16 ireturn
17 astore 4
19 iconst_3
20 istore_1
21 aload 4
23 athrow 

Exception table:

Nr.Start PcEnd PcHandler PcCatch Type
0048cp info #2 <java/lang/Exception>
10417cp info #0 < any>
281317cp info #0 < any>
3171917cp info #0 < any>

通过异常表,我们知道,编译器为这段Java代码生成了4条异常表记录,对应4条可能出现的代码执行路径。我们看前3条记录,这3条是能和我们的代码对应上的,分别对应下面的3种情况为:

  • 如果try语句块中出现属于Exception或其子类的异常,则转到catch语句块处理。
  • 如果try语句块中出现不属于Exception或其子类的异常,则转到finally语句块处理。
  • 如果catch语句块中出现任何异常,则转到finally语句块处理。

第4条记录的意思是,从17行指令到19行指令(不包括19行指令)之间出任何异常,会重新跳到17行指令开始执行。这好像进入了一个死循环。其实,第4行记录,我觉得是不会发生的特殊情况,我不太懂为什么生成Class文件时会给我们在异常表中记录这一情况!所以,就先暂时抛开它,我们去看前3条记录。

从字节码层面分析异常处理

返回到我们上面的问题,这段代码的返回值应该是多少?熟悉Java的读者应该很容易说出答案:如果没有异常,返回值是1;如果出现Exception异常,返回值是2;如果出现了Exception以外的异常,方法非正常退出,没有返回值。我们一起来分析一下字节码的执行过程(结合异常表),从字节码的层面上看看为何会有这样的返回结果。

字节码中0~ 3行所做的操作就是将整数1赋值给变量x,并且将此时x的值赋值一份副本到本地变量表索引为2的位置处。如果这过程没有出现异常,则继续往后执行4~ 5行(即finally语句块的代码),将变量x重新赋值为3。最后执行6~ 7行,把之前存在局部变量表2位置处的int型常量1又推送到栈顶,然后ireturn指令把栈顶元素弹出,即方法返回int型常量1,方法结束。这是正常流程。

如果0~ 3行出现异常,且属于Exception或其子类的异常,那么根据异常表第1条记录,JVM就知道如何处理。此时,PC寄存器指针转到第8行(即catch语句块的代码),8~ 16行所做的事情,先将2赋值给变量x,再把常量2保存在局部变量表中,接着重新把常量3赋值给变量x,最后从局部变量中从新获取常量2推送至栈顶,最后ireturn指令把栈顶元素弹出,即方法返回int型常量2,方法结束。

如果0~ 3行出现异常,且不属于Exception或其子类的异常,那么根据异常表第2条记录,就会转到17行(即直接进入到finally语句块),就会执行17~ 23行的指令。

如果8~ 12行出现异常,出现任何异常,那么根据异常表第3条记录,也会转到17行(即直接进入到finally语句块),就会执行17~ 23行的指令。

于是,我重新给上面的字节码做了注释,方便大家阅读理解。

 0 iconst_1  // 将一个int型常量1,推送栈顶
 1 istore_1  // 将1个int类型数据保存到局部变量表的索引为1的位置中
 2 iload_1   // 将存放在局部变量索引为 1 的位置的int型数据推送至栈顶
 3 istore_2  // 将1个int类型数据保存到局部变量表的索引为 2 的位置中
     
 // 4-5行的指令,其实就是对应finall语句块的Java代码    
 4 iconst_3  // 将一个int型常量3,推送栈顶
 5 istore_1  // 将1个int类型数据保存到局部变量表的索引为 1 的位置中
     
 6 iload_2   // 将存放在局部变量索引为 2 的位置的int型数据推送至栈顶
 7 ireturn   // 结束方法,并返回一个int类型数据   
   
 8 astore_2  // 将一个reference类型数据保存到局部变量表的索引为 2 的位置中
 9 iconst_2  // 将一个int型常量2,推送栈顶
10 istore_1  // 将1个int类型数据保存到局部变量表的索引为 1 的位置中
11 iload_1   // 将存放在局部变量索引为 1 的位置的int型数据推送至栈顶
12 istore_3  // 将1个int类型数据保存到局部变量表的索引为 3 的位置中

// 13-14行的指令,其实就是对应finall语句块的Java代码    
13 iconst_3  // 将一个int型常量3,推送栈顶
14 istore_1  // 将1个int类型数据保存到局部变量表的索引为 1 的位置中

15 iload_3   // 将存放在局部变量索引为 3 的位置的int型数据推送至栈顶
16 ireturn   // 结束方法,并返回一个int类型数据

17 astore 4  // 将一个reference类型数据保存到局部变量表的索引为 4 的位置中

// 19-20行的指令,其实就是对应finall语句块的Java代码    
19 iconst_3  // 将一个int型常量3,推送栈顶
20 istore_1  // 将1个int类型数据保存到局部变量表的索引为 1 的位置中

21 aload 4   // 将存放在局部变量索引为 4 的位置的reference型数据推送至栈顶
23 athrow    // 抛出一个异常实例(exception 或者 error)

小结一下

从字节码层面分析了虚拟机在处理异常流程的过程,我们可以看出以下几点内容:

  • 异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。(注:在JDK1.4.2之前的Javac编译器采用了jsr和ret指令实现finally语句。在JDK1.7中,已经完全禁止Class文件中出现jsr和ret指令,如果遇到这两条指令,虚拟机会在类加载的字节码校验阶段抛出异常)
  • 当异常处理存在finally语句块时,编译器会自动在每一段可能的分支路径之后都将finally语句块的内容冗余生成一遍来实现finally语义。
  • 在我们Java代码中,finally语句块是在最后的,但编译器在生成字节码时候,其实将finally语句块的执行指令移到了ireturn指令之前,指令重排序了。所以,从字节码层面,我们解释了,为什么finally语句总会执行!初学Java的时候,我们有感到困惑,为什么方法已经return了,finally语句块里的代码还会执行呢?这是因为,在字节码中,它就是先执行了finally语句块,再执行return的,而这个变化是Java编译器帮我们做的,程序员一脸闷逼。
  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值