直面底层之字节码看try-catch-finally

1,323 阅读10分钟

前言

我们都知道 try catch finally 语法快 finally的代码一定会执行,但是其底层的原理知道么?本文直面底层看下 为什么finnaly 语法块不管是正常情况下还是 抛出异常情况下都会执行


一、没有return 的 try catch finally

我们先看一个简单的示例:

//java代码
public static void main(String[] args) {
    try {
        int a = 1/1;// int a = 1/0
        System.out.println("正常执行");
    }catch (Exception e){
        System.out.println("Exception");
    }finally {
        System.out.println("这里执行了");
    }
}

上述代码很简单,当 int a = 1/1 时 输出了 “正常执行” 、“这里执行了”,try 和 finally 代码块执行了

当 int a = 1/0 时 输出了 “Exception”、“这里执行了”,catch 和 finally 的代码块都执行了

接下来我们看下字节码

//java 代码  int a = 1/1 字节码 
public static void main(java.lang.String[]);
    Code:
       //0- 9行 执行try 代码块
       0: iconst_1
       1: istore_1
       2: getstatic     #2   // Field java/lang/System.out:Ljava/io/PrintStream;
       5: ldc           #3   // String 正常执行
       7: invokevirtual #4   // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      //10-18 这里执行的是 finally 的代码块,然后跳转至 52行 return
      10: getstatic     #2   // Field java/lang/System.out:Ljava/io/PrintStream;
      13: ldc           #5   // String 这里执行了
      15: invokevirtual #4  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      18: goto          52
      //21-30从下面异常表第一行 可知 如果0-9行抛出异常 会跳转至21行
      //存储抛出的异常 输出 “Exception”
      21: astore_1
      22: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
      25: ldc           #7  // String Exception
      27: invokevirtual #4  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      //30-38 这里执行的是 finally 的代码块 然后跳转至 52行 return
      30: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
      33: ldc           #5  // String 这里执行了
      35: invokevirtual #4  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      38: goto          52
      //这里对应的是 异常表的第2 和 第三行 也就是 0-9行抛出了非 Exception的异常 或者 21-29行 抛出了任何异常 会跳转至 41行处理 
      41: astore_2      //存储抛出的异常到局部变量表
      //输出"这里执行了"
      42: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
      45: ldc           #5  // String 这里执行了
      47: invokevirtual #4  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      50: aload_2  //取出异常 并抛出
      51: athrow
      52: return
    Exception table:
       from    to  target type
           0    10    21   Class java/lang/Exception
           0    10    41   any
          21    30    41   any

首先我们看到下面多了一个 Exception table: 有from、to、target、type

每一行的意义:from/to 表示 区间行数,from包含,to 不包含; target 和 type 表示 如果异常类型是 type 的话 跳转至 target 所在的行数执行

我们以第一行 为例,也就是 在 字节码的第 0 到 9 行(包含),如果执行期间有异常且是type 为 java/lang/Exception 则跳转至21行执行;any 表示除了Exception 外的其他异常。

经过上述字节码分析(看注释),try catch finally 会生成异常表来辅助代码执行,而确保finally 一定执行 是在 try catch 代码块内都复制了一份 finally的代码块。

二、包含return 的 try catch finnaly

上部分分析了 try-catch -finnaly 能确保 finally 代码块一定会执行的原理,那 如果 有返回值呢,会返回什么?

我们开看下示例代码:

public static int testReturn(){
    try {
        int a = 1/0; //int a = 1/1;
        return 0;
    }catch (Exception e){
        return 1;
    }finally {
        return 2;
    }
}

public static void main(String[] args) {
    System.out.println(testReturn());
}

上述代码的测试结果无论会不会抛异常,输出结果都是 2.

那我们看下字节码只看下 testReturn 的:

  public static int testReturn();
    Code:
       //0-7 对应的 try 代码块,加入了finnaly代码块 
       0: iconst_1   //将1 加载到操作数栈
       1: iconst_0   //将0 加载到操作数栈
       2: idiv       //计算 1/0
       3: istore_0   //存储计算结果
       4: iconst_0   //将0 加载到栈顶 
       5: istore_1   //将 0 存入局部变量表 但未返回
       6: iconst_2   //将2 加载到栈顶
       7: ireturn    // 返回
       //8-12 对应 catch代码块 加入了finally 代码块
       8: astore_0   //存储抛出的异常
       9: iconst_1   //将1 加载到栈顶
      10: istore_1   //将1存储到局部变量表 但未返回
        //11-12行 
      11: iconst_2   //将2 加载到栈顶 
      12: ireturn    //返回 2
        //finally代码块
      13: astore_2
      14: iconst_2
      15: ireturn
    Exception table:
       from    to  target type
           0     6     8   Class java/lang/Exception  //0-5行如果有异常跳转至 第8行
           0     6    13   any          //0-5行如果其他异常 跳转至 13行
           8    11    13   any          //8-10行如果有其他异常 跳转至 13行

可以看到 为了 确保 finally 代码块一定执行,在 try-catch 中返回的时候只是做了存储到局部变量表,真正返回的还是 复制到各个代码块的finally 代码块,所以返回的 都是 2.(试想下如果返回的不是 2 那不就代表了 finally 代码块不一定会执行 )

如果要在try-catch-finally 中有返回值,请谨慎使用,因为 finally 一定会执行,如果finally 有返回值,则一定会执行 finally 的返回值;如果需要 try 和 catch 中的返回值,则finally 中的return语句可以不写。

三、try--with--resource的原理

try-with-resources 是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch 语句块中使用的资源。所谓的资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources 语句确保了每个资源在语句结束时关闭。所有实现了 java.lang.AutoCloseable 接口(实现了 java.io.Closeable 的所有对象)都可以用上述用法。那么保证资源对象一定关闭,是实现了finally 么 ?

我们看下如下示例:

void readFile() {

    try (
            FileReader fr = new FileReader("d:/input.txt");
            BufferedReader br = new BufferedReader(fr)
    ) {
        String s = "";
        while ((s = br.readLine()) != null) {
            System.out.println(s);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

直接看下字节码:

 void readFile();
    Code:
       /
       * 生成 FileReader     放入 局部变量表 位置 1
       * 将 null             放入 局部变量表  位置 2
       * 生成 BufferedReader 放入 局部变量表 位置 3
       * 将 null             放入 局部变量表 位置 4
       * 将字符串s 初始值 “”   放入 局部变量表 位置 5
       */
       
       0: new           #2   // class java/io/FileReader
       3: dup
       4: ldc           #3  // String d:/input.txt
       6: invokespecial #4  // Method java/io/FileReader."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: aconst_null
      11: astore_2
      12: new           #5 // class java/io/BufferedReader
      15: dup
      16: aload_1
      17: invokespecial #6 // Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
      20: astore_3
      21: aconst_null
      22: astore        4
      24: ldc           #7 // String
      26: astore        5
      
      /*
      *加载 BufferedReader 并 调用 readLine 赋值给 s
      **/
      28: aload_3
      29: invokevirtual #8 // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
      32: dup
      33: astore        5
      /*
      * 判断 s 是否为null 不为null 则执行完后 继续跳转至 28行,如果为空 跳转至 49行
      */
      35: ifnull        49
      38: getstatic     #9 // Field java/lang/System.out:Ljava/io/PrintStream;
      41: aload         5
      43: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      46: goto          28
        
      /*
      *判断 BufferedReader是否为null 如果为null 跳转至 130行,不为null 局部变量表第四个位置是否为null
      * 如果 为null 跳转至 77行 ,否则执行 BufferedReader close 跳转至 130行
      */
      49: aload_3
      50: ifnull        130
      53: aload         4
      55: ifnull        77
      58: aload_3
      59: invokevirtual #11  // Method java/io/BufferedReader.close:()V
      62: goto          130
   
      /** 
      *执行 58-61行即 BufferedReader.close 如果抛出异常 会进入此
      * 存储抛出的异常 到局部变量表位置 5这里 原位置的 s 已经没用了覆盖成了异常 
      * 取出 变量表 4 和 5 位置的异常,调用 Throwable.addSuppressed 拼接异常
      **/
      65: astore        5  
      67: aload         4
      69: aload         5
      71: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
      74: goto          130
        
      //调用 BufferedReader close 跳转至 130行
      77: aload_3
      78: invokevirtual #11 // Method java/io/BufferedReader.close:()V
      81: goto          130
        
      /** 
      *执行 24-48 行  如果抛出Throwable 异常 会进入此
      * 存储抛出的异常 到局部变量表位置 5这里 原位置的 s 已经没用了覆盖成了异常 
      * 位置的异常,调用 Throwable.addSuppressed 拼接异常
      **/
        
      84: astore        5
      86: aload         5
      88: astore        4
      90: aload         5
      92: athrow
      /*
      *存储 24-48  84-95 抛出的非 throwable 异常  尝试调用 BufferedReader.close
      */
      93: astore        6
      95: aload_3
      96: ifnull        127
      99: aload         4
     101: ifnull        123
     104: aload_3
     105: invokevirtual #11 // Method java/io/BufferedReader.close:()V
     108: goto          127
       
     /*
     *存储 104 -108 BufferedReader.close 抛出的异常 并拼接异常
     */
     111: astore        7
     113: aload         4
     115: aload         7
     117: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
     120: goto          127
       
   
     123: aload_3
     124: invokevirtual #11 // Method java/io/BufferedReader.close:()V
       
     //抛出异常
     127: aload         6
     129: athrow
       
     /*
      *判断 FileReader 是否为null 如果为null 跳转至 201行,不为null 局部变量表第2个位置是否为null
      * 如果 为null 跳转至 154 行 ,否则执行 BufferedReader close 跳转至 201 行
      */
     130: aload_1
     131: ifnull        201
     134: aload_2
     135: ifnull        154
     138: aload_1
     139: invokevirtual #14 // Method java/io/FileReader.close:()V
     142: goto          201
       
     /*
     *存储 138 -144 FileReader.close 抛出的异常 并拼接异常
     */
     145: astore_3
     146: aload_2
     147: aload_3
     148: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
     151: goto          201
       
     
     154: aload_1
     155: invokevirtual #14 // Method java/io/FileReader.close:()V
     158: goto          201
       
      /*
     *存储 12 -129 行 抛出的Throwable异常 
     */
     161: astore_3
     162: aload_3
     163: astore_2
     164: aload_3
     165: athrow
       
      /*
     *存储 12 -129 行 抛出的非 Throwable异常 
     */
     166: astore        8
     168: aload_1
     169: ifnull        198
     172: aload_2
     173: ifnull        194
     176: aload_1
     177: invokevirtual #14 // Method java/io/FileReader.close:()V
     180: goto          198
       
       
     183: astore        9
     185: aload_2
     186: aload         9
     188: invokevirtual #13  // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
     191: goto          198
       
       
     194: aload_1
     195: invokevirtual #14 // Method java/io/FileReader.close:()V
     
     198: aload         8
     200: athrow
       
     /*
     * 跳转至 209行
     */
     201: goto          209
       
    /*
    * 只有异常表最后一行 0-200行抛出异常时会进入这里,未捕获的异常输出
    */
     204: astore_1
     205: aload_1
     206: invokevirtual #16  // Method java/lang/Exception.printStackTrace:()V
     
     /*
     * 执行结束
     */
     209: return
    Exception table:
       from    to  target type
          58    62    65   Class java/lang/Throwable
          24    49    84   Class java/lang/Throwable
          24    49    93   any
         104   108   111   Class java/lang/Throwable
          84    95    93   any
         138   142   145   Class java/lang/Throwable
          12   130   161   Class java/lang/Throwable
          12   130   166   any
         176   180   183   Class java/lang/Throwable
         161   168   166   any
           0   201   204   Class java/lang/Exception

这么长的字节码一看就傻眼了,莫慌 我们有策略的分析下。

策略1: 我们按照 异常表的target 行数,将 字节码分成几块
策略2: 将字节码中 有if 判断跳转的 和 goto 跳转的 分块

分完块后我们 来看下每块的内容,逐个分析

正常的执行流程:

35行判断 s为null 进入 49(50)行,

49(50) 判断 BufferedReader 不为null,且没有异常,执行 close 进入 130行

130-139FileReader 不为null 且没有异常 执行 close 进入 201行 然后进入 209结束

异常执行流程:

我们挑一个流程 假设 24-49 抛出了 Throwable,我们看下流程:

代码会进入 84行

84-108: 处理异常,如果 BufferedReader 不为null 执行 BufferedReader.close,并进入 127行

127-142:处理异常 并执行 FileReader.close 并进入 201 行 然后进入 209行 结束

小结:
  • 第一,try-with-resource 语法并不是简单的在 finally 里中加入了closable.close()方法,因为 finally 中的 close 方法如果抛出了异常会淹没真正的异常;
  • 第二,引入了 suppressed 异常的概念,能抛出真正的异常,且会调用 addSuppressed 附带上 suppressed 的异常