从字节码层面看try-catch-finally的实现机制

考虑以下方法可能的执行流程

    public static int A() {
        try {//代码块1
            return B();
        }catch (Exception e){//代码块2
            C();
        }
        finally {//代码块3
            System.out.println("3");
        }//代码块4
        System.out.println("4");
        return 4;
    }

1.方法B正常返回:代码块1 -> 代码块3 ->代码块1返回

2.方法B抛出Exception:代码块1 -> 代码块2 ->代码块4 -> 代码块3->代码块4返回

3.方法B抛出Error:代码块1抛出Error-> 代码块3 ->Error被抛向上层

4.方法B抛出Exception,同时方法C抛出异常:代码块1 -> 代码块2抛出异常 ->代码块3 ->异常被抛向上层

可以看出不论哪种情况,代码块3也就是finally块都好像是打断了其前一步代码块的执行,执行完毕后再将控制权返回给原代码块。老版本的jvm是通过jsr和ret指令来实现这一过程的,在jsr指令执行前将被打断处下一个指令的地址保存在局部变量表中,jsr跳转到finally块执行,执行完后通过ret指令及前面保存的地址来恢复被打断代码的执行。目前的jvm通过复制finally块的字节码到原jsr指令处来实现这一过程,下面是上述代码对应的字节码:

 

上面红框中的字节码是jvm为了实现finally关键字的语义而复制的,其首先会保存操作数栈顶的值到局部变量表中,接着找到System.out的直接引用以及传入println()方法的参数,并将它们压栈,然后调用println()方法,最后从局部变量表中恢复最初保存的栈顶元素。这里的栈顶元素可能是被打断代码将要返回上层调用栈的返回值,或者前面没有catch住的异常等等,因此需要进行保存。

对于执行流程1,直接从0行字节码顺序执行到13行的字节码并返回。

对于执行流程2,在0行字节码处出现Exception,根据下面的异常跳转表第一条,跳转到14行执行。首先保存栈顶的异常对象的引用到局部变量表索引0处(前面第一个红框里的字节码也用到了索引0的槽位,但两者的作用域没有交集,因此可以复用局部变量表的槽位),然后调用方法C(),由于该方法返回时会将其返回值压入调用者的栈顶,而当前并不需要使用这个返回值,因此直接使用pop指令,而不是store指令将其保存在局部变量表中。接着执行finally代码块,并在最后跳转到41行执行方法中剩下的代码。

对于执行流程3,在0行字节码处出现Error,根据下面的异常跳转表第二条,跳转到30行并顺序执行到40行,抛出没有catch住的Error,此时发现异常跳转表中找不到对应的条目,因此会将这个异常抛向上层栈帧,后面的字节码也不会执行。

对于执行流程4,15行之前的执行过程同流程2,15行出现异常,根据下面的异常跳转表第三条,跳转到30行,后续执行同流程3。

如果在finally块中加入return语句,或者finally块中代码执行时抛出了异常,那么其对应字节码的最后一条会是类似ireturn的指令(也可能是areturn等等,取决于返回值的类型),或者是athrow指令,不论是哪条指令,执行完后都会返回上一层栈帧,而不会将控制权返回给”被打断“的代码块,此时原先的返回值或异常对象都会因当前栈帧的回收而失去指向他们的引用,进而被当作垃圾回收。finally块中产生的返回值或异常会代替原先的值进入上一层栈帧的操作数栈中。

参考资料:深入Java虚拟机(原书第2版)

猜你喜欢

转载自blog.csdn.net/qq_37720278/article/details/86688550
今日推荐