字节码角度看面试题 —— try catch finally 为啥 finally 语句一定会执行

字节码角度看面试题 —— try catch finally 为啥 finally 语句一定会执行

    • 一、try catch 字节码分析
      • 1.1 一个 catch
      • 1.2 多个 catch
    • 二、finally 字节码分析
    • 三、小结
    • 四、留道题

一、try catch 字节码分析

1.1 一个 catch

public class Test {
    public void foo() {
        try {
            tryItOut();
        } catch (MyException e) {
            handleException(e);
        }
    }
    
    private void tryItOut() throws MyException {
    
    }
    
    class MyException extends Exception {
    
    }
    
    private void handleException(MyException e) {
    
    }
}

javap 查看字节码

javap -c -v Test

复制 foo() 函数部分,如下

public void foo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: aload_0
         1: invokespecial #15                 // Method tryItOut:()V
         4: goto          13
         7: astore_1
         8: aload_0
         9: aload_1
        10: invokespecial #18                 // Method handleException:(LTest$MyException;)V
        13: return
      Exception table:
         from    to  target type
             0     4     7   Class Test$MyException

14 ~ 16 行:在生成的字节码中,每个方法都会附带一个异常表(Exception table),其中每一行表示一个异常处理器,由 from 指针、to 指针、target 指针、所捕获的异常类型 type 组成
这些指针的值是字节码索引,用于定位字节码,其含义是在 [from, to) 字节码范围内,抛出异常类型为 type 的异常,就会跳转到 target 表示的字节码处

上面的栗子,异常表表示:在 0 到 4 中间(不含 4)如果抛出了 MyException 的异常,就跳转到 7 执行

  • 9 行:astore_1 指令处于异常表范围内,因此表示将异常对象 Test$MyException 的引用放到局部变量表下标为 1 的位置
  • 10 行:aload_0 指令表示将 this(非静态方法,局部变量表第 0 个位置默认就是 this)引用放到操作数栈栈顶
  • 11 行:aload_1 指令表示将对象 Test$MyException 的引用放到操作数栈栈顶
  • 12 行:invokespecial 指令表示弹出栈顶的两个元素,去调用 handleException() 方法
  • 13 行:return 指令表示函数执行完了,返回

1.2 多个 catch

修改上面的栗子,多加几个 catch 块

public class Test {
    public void foo() {
        try {
            tryItOut();
        } catch (MyException e) {
            handleException(e);
        } catch (MyException1 e) {
            handleException1(e);
        }
    }
    
    private void tryItOut() throws MyException, MyException1 {
    
    }
    
    class MyException extends Exception {
    
    }
    
    private void handleException(MyException e) {
    
    }
    
    class MyException1 extends Exception {
    
    }
    
    private void handleException1(MyException1 e) {
    
    }
}

javap 查看字节码

javap -c -v Test

复制 foo() 函数部分,如下

public void foo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: aload_0
         1: invokespecial #15                 // Method tryItOut:()V
         4: goto          22
         7: astore_1
         8: aload_0
         9: aload_1
        10: invokespecial #18                 // Method handleException:(LTest$MyException;)V
        13: goto          22
        16: astore_1
        17: aload_0
        18: aload_1
        19: invokespecial #22                 // Method handleException1:(LTest$MyException1;)V
        22: return
      Exception table:
         from    to  target type
             0     4     7   Class Test$MyException
             0     4    16   Class Test$MyException1

很明显可以看出,异常表多了一行记录

Java 虚拟机会从上到下遍历异常表中所有的条目。当触发异常表的字节码索引值在某个异常条目范围内,则会判断抛出的异常与该条目想捕获的异常是否匹配,当前栗子即为在 0 到 4 中间(不含 4)可能会抛出 MyException 或 MyException1 异常,具体抛哪个,虚拟机会去匹配实际抛出的异常与该条目想捕获的异常是否匹配

  • 如果匹配,Java 虚拟机会将控制流跳转到 target 指向的字节码;若不匹配,则继续遍历异常表
  • 如果遍历完异常表的所有异常条目,还未匹配到异常处理器,那么该异常将蔓延到调用方中重复上述操作。最坏的情况下虚拟机需要遍历该线程 Java 栈上所有方法的异常表

二、finally 字节码分析

public class Test {
    public void foo() {
        try {
            tryItOut();
        } catch (MyException e) {
            handleException(e);
        } finally {
            handleFinally();
        }
    }
    
    private void tryItOut() throws MyException {
    
    }
    
    class MyException extends Exception {
    
    }
    
    private void handleException(MyException e) {
    
    }
    
    private void handleFinally() {

	}
}

javap 查看字节码

javap -c -v Test

复制 foo() 函数部分,如下

public void foo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: invokespecial #15                 // Method tryItOut:()V
         4: goto          27
         7: astore_1
         8: aload_0
         9: aload_1
        10: invokespecial #18                 // Method handleException:(LTest$MyException;)V
        13: aload_0
        14: invokespecial #22                 // Method handleFinally:()V
        17: goto          31
        20: astore_2
        21: aload_0
        22: invokespecial #22                 // Method handleFinally:()V
        25: aload_2
        26: athrow
        27: aload_0
        28: invokespecial #22                 // Method handleFinally:()V
        31: return
      Exception table:
         from    to  target type
             0     4     7   Class Test$MyException
             0    13    20   any

24 ~ 27 行:表示在 0 到 4 中间(不含 4)如果抛出了 MyException 的异常,就跳转到 7 执行;在 0 到 13 中间(不含 13)如果抛出了 any 类型的异常(也就是未捕获的异常),就跳转到 20 执行

  • 6 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
  • 7 行:invokespecial 指令表示调用 tryItOut() 方法
  • 8 行:goto 27 指令表示程序流程跳转到 27(21 行),aload_0 指令表示将 this 引用放到操作数栈栈顶
  • 9 行:首先要明白 try 代码块中的代码按理说已经执行完了,如果还能执行到走到 9 行,只能说明遇到异常了,另一方面也能知道,因为 9 行对应的 7 在异常表范围内,所以发生了异常,那么astore_1 指令表示将异常对象 MyException 的引用放到局部变量表下标为 1 的位置
  • 10 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
  • 11 行:aload_1 指令表示将异常对象 Test$MyException 的引用放到操作数栈栈顶
  • 12 行:invokespecial 指令表示弹出栈顶两个元素,去调用 handleException() 方法
  • 13 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
  • 14 行:invokespecial 指令表示调用 handleFinally() 方法
  • 15 行:goto 31 指令表示程序流程跳转到 31(23 行),return 指令表示函数执行完了,返回
  • 16 行:16 行对应 20,在异常表的范围内,因此 astore_2 指令表示将异常对象 Exception 的引用放到局部变量表下标为 2 的位置
  • 17 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
  • 18 行:invokespecial 指令表示调用 handleFinally() 方法
  • 19 行:aload_2 指令表示将异常对象 Exception 的引用放到操作数栈栈顶
  • 20 行:athrow 指令表示抛 catch 代码块执行过程中出现的异常
  • 21 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
  • 22 行:invokespecial 指令表示调用 handleFinally() 方法
  • 23 行:return 指令表示函数执行完了,返回

上面的字节码其实对应代码的几种可能的执行流程
情况 ①:try 代码块正常执行,没有异常,对应上面的字节码第 6、7、8、21、22、23 行
// 当前情况下,22 行对应 finally 代码块的执行
情况 ②:try 代码块执行过程中遇到异常,且该异常在异常表范围内有匹配的异常(也就是说有对应的 catch 块捕获了出现的异常),对应上面的字节码第 6、7、9、10、11、12、13、14、15、23 行
// 当前情况下,14 行对应 finally 代码块的执行
情况 ③:try 代码块执行过程中遇到异常,且该异常在异常表范围内有匹配的异常,但是 catch 块中的代码执行过程中出现了异常,对应上面的字节码第 6、7、9、10、11、12、13、16、17、18、19、20 行
// 当前情况下,18 行对应 finally 代码块的执行,注意 throw 指令执行是在 20 行,也就是说如果在 catch 代码块中即使出现异常,也会先执行 finally 代码块,然后再抛 catch 块中出现的异常

综上所述,不管什么情况下,finally 代码块都会得到执行(●°u°●)​ 」

三、小结

  • JVM 采用异常表的方式来处理 try catch 的跳转逻辑
  • finally 的实现采用拷贝 finally 语句块的方式来实现 finally 一定会执行的语义逻辑

四、留道题

public static int foo() {
    int x = 0;
    try {
        return x;
    } finally {
        ++x;
    }
}

public static void main(String[] args) {
    int res = foo();
    System.out.println(res);
}

返回 0

你可能感兴趣的:(Java基础)