从字节码层面讲解finally

4 篇文章 0 订阅

从字节码层面讲解finally

起因

一直就没太明白java的try catch finally 的语句,只是从语法层面上理解为,如果try的语句里发生异常,就跳转到catch语句执行,不管发没发生异常,finally都会执行。这样理解平常也够用了,但是总觉得有点虚,没实际理解实现原理。最近看一些教程,讲解字节码。现在尝试从字节码层面理解try catch finally。

一个笔试题

https://blog.csdn.net/huangzhilin2015/article/details/114157287

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

  • 如果try语句没有出现属于Exception或其子类的异常,返回值为1
  • 如果出现,返回值为2
  • 如果出现Exception以外的其它异常,则没有返回,方法异常退出

这里可能会疑惑,finally不是一定执行的吗,为什么返回值不是3?

public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=5, args_size=1
         0: iconst_1 //将int类型常量1压入操作数栈-> 操作数栈:1,局部变量表:空
         1: istore_1 //将int类型值出栈,存入局部变量1-> 操作数栈:空,局部变量表:slot1=1,也就是x=1
         2: iload_1 //从局部变量表1中装载int类型值,压入操作数栈-> 操作数栈:1,局部变量表:slot1=1
         3: istore_2 //将int类型值出栈,存入局部变量2-> 操作数栈:空,局部变量表:slot1=1,slot2=1
         4: iconst_3 //将int类型常量3压入操作数栈-> 操作数栈:3,局部变量表:slot1=1,slot2=1
         5: istore_1 //将int类型值出栈,存入局部变量1-> 操作数栈:空,局部变量表:slot1=3,slot2=1
         6: iload_2 //从局部变量表2中装载int类型值,压入操作数栈-> 操作数栈:1,局部变量表:slot1=3,slot2=1
         7: ireturn //返回int类型,此时操作数栈顶为1,所以返回1(无异常情况)
         8: astore_2 //当0到3行出现异常跳至这里,将catch中的Exception e复制,存入局部变量表2->slot2 = e
         9: iconst_2 //将int类型常量2压入操作数栈-> 操作数栈:2,局部变量表:slot2=e
        10: istore_1 //将int类型值出栈,存入局部变量表1-> 操作数栈:空,局部变量表:slot1=2
        11: iload_1 //从局部变量表1中装载int类型值,压入操作数栈-> 操作数栈:2,局部变量表:slot1=2
        12: istore_3 //将int类型值出栈,存入局部变量表3-> 操作数栈:空,局部变量表:slot1=2,slot3=2
        13: iconst_3 //将int类型常量3压入操作数栈-> 操作数栈:3,局部变量表:slot1=2,slot3=2
        14: istore_1 //将int类型值出栈,存入局部变量表1-> 操作数栈:空,局部变量表:slot1=3,slot3=2
        15: iload_3 //从局部变量表3中装载int类型值,压入操作数栈-> 操作数栈:2,局部变量表:slot1=3,slot3=2
        16: ireturn //返回int类型,此时操作数栈顶为2,所以返回2(Exception 情况)
        17: astore        4 //不属于Exception及其子类的异常存入局部变量表4-> 局部变量表:slot4=异常引用
        19: iconst_3 //将int类型常量3压入操作数栈-> 操作数栈:3,局部变量表:slot4=异常引用
        20: istore_1 //将int类型值出栈,存入局部变量表1-> 操作数栈:空,局部变量表:slot1=3,slot4=异常引用
        21: aload         4 //将局部变量表4中的异常引用压入栈顶
        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

简单提一句,字节码层面理解try catch finally的话,编译器自动在每段可能的分支路径之后都将finally语句块的内容冗余生成一遍来实现finally语义。

也就是说,finally的在try里,每个catch里都会编译一次。

没发生异常,正常执行的话,finally的字节码其实也编译在里面了,会执行。

而如果发生异常的话,根据异常处理表。在异常发生的代码行里,发生了可以处理的异常,就会跳转执行到catch的字节码。主要是存异常,执行catch字节码,执行finally的字节码。

结合这个异常表和Code中的注释,可以发现,如果try语句中发生了Exception及其子类异常,那么执行的字节码为第8-16行,最终返回值为2。其他异常的话,则跳到第17行处理,执行第17-23行,最终将异常抛出,方法值没有返回。

从异常表中还可以发现另一问题,在catch块中如果出现了异常(第8到12行),那么也会跳到第17行进行处理,也就是执行finally代码块。

那为什么不是返回3,而是返回2呢?

深入理解finally的设计思路

https://www.jb51.net/article/74771.htm

其中必须至少存在一个catch子句或finally子句。try语句的主体将一直执行,直到抛出异常或主体成功完成为止。如果抛出异常,则从第一个到最后一个依次检查每个catch子句,以查看异常对象的类型是否可分配给catch中声明的类型。当找到一个可分配的catch子句时,它的块被执行,其标识符被设置为引用异常对象。不会执行其他catch子句。任何数量的catch子句(包括零)都可以与特定的TRy关联,只要每个子句捕获不同类型的异常。如果没有找到合适的catch,异常将从try语句渗透到可能有catch子句来处理它的任何外部try中。

   public static int getValue() {
        int i = 1;
        try {
            i = 4;

        } finally {
            i++;
        }
        return i;

    }

字节码

 public static int getValue();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=0
         0: iconst_1
         1: istore_0
         2: iconst_4
         3: istore_0
         4: iinc          0, 1
         7: goto          16
        10: astore_1
        11: iinc          0, 1
        14: aload_1
        15: athrow
        16: iload_0
        17: ireturn
      Exception table:
         from    to  target type
             2     4    10   any

返回值为5

    public static int getValue() {
        int i = 1;
        try {
            i = 4;
            return i;
        } finally {
            i++;
        }
    }
 public static int getValue();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=0
         0: iconst_1
         1: istore_0
         2: iconst_4
         3: istore_0
         4: iload_0
         5: istore_1
         6: iinc          0, 1
         9: iload_1
        10: ireturn
        11: astore_2
        12: iinc          0, 1
        15: aload_2
        16: athrow
      Exception table:
         from    to  target type
             2     6    11   any

返回值为4

Java 虚拟机是如何编译 finally 语句块。实际上,Java 虚拟机会把 finally 语句块作为 subroutine(对于这个 subroutine 不知该如何翻译为好,干脆就不翻译了,免得产生歧义和误解。)直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。请注意,前文中我们曾经提到过 return、throw 和 break、continue 的区别,对于这条规则(保留返回值),只适用于 return 和 throw 语句,不适用于 break 和 continue 语句,因为它们根本就没有返回值。

20151112151408089.jpg (577×412)

简单来说,如果try里有return 和 throw 语句,finally的执行会复制一个值到本地变量表。因此如果返回值是基本类型的话,会出现比较奇怪的结果,finnaly好像没执行的样子。如果是对象,数组的话,有执行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值