从字节码层面聊聊try catch finally

1,326 阅读5分钟

概述

刚入坑java的时候, 经常有类似如下的面试题,并询问执行结果以及为什么?

public class TestTryCatchFinally {

    public static void main(String[] args) {
        try {
            System.out.println("try code");
            testMethodThrowException();
        } catch (RuntimeException ex) {
            System.out.println("catch code");
            throw ex;
        } finally {
            System.out.println("finally code");
        }
    }

    private static void testMethodThrowException() {
        throw new RuntimeException("testMethodThrowException...");
    }

}

输出如下:
try code
catch code
finally code
Exception in thread "main" java.lang.RuntimeException: testMethodThrowException...
	at TestTryCatchFinally.testMethodThrowException(TestTryCatchFinally.java:25)
	at TestTryCatchFinally.main(TestTryCatchFinally.java:15)

下面我们将通过以下几点来分析try/catch/finally执行机制:

  1. try/catch/finally语义
  2. 在字节码层面是如何处理的
  3. jdk7之后的语法糖

try/catch/finally语义

try {
    ...
} catch(XxxException e) {
    ...
} finally {
    ...
}
  1. try{}中的语句块为监控可能发生异常的代码块。
  2. catch(){}用来捕获异常。当try{}触发异常时,通过匹配catch中的异常类型进行捕获处理。需要注意的是多个catch块从上往下依次匹配,并且前面的Exception范围不能包含下面的Exception,否则会在编译期间报错。
  3. finally{}主要用于做一些资源清理工作。

try可以随意搭配catch,finally其中一个,或两者都搭配使用。 (但不能单独使用try,这也失去了try的意图)

在字节码层面是如何处理的

将前面概述中的代码通过javap命令翻译成字节码,下面是字节码内容:

下面的字节码简化了一些信息,并标记了一些简单的注释。

public class com.example.leetcode.jvm.day1.TestTryCatchFinally
  SourceFile: "TestTryCatchFinally.java"
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   ... //常量池
{
  public com.example.leetcode.jvm.day1.TestTryCatchFinally();
    ... // 默认构造方法字节码

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         // 获取System.out标准输出静态属性
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 将常量池字符串"try code"压入操作数栈顶
         3: ldc           #3                  // String try code
         // 调用println方法输出
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         // 调用testMethodThrowException()静态方法
         8: invokestatic  #5                  // Method testMethodThrowException:()V
        // 获取System.out标准输出静态属性
        11: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        // 将常量池字符串"finally code"压入操作数栈顶
        14: ldc           #6                  // String finally code
        // 调用println方法
        16: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       // 跳转到字节码索引行44 return
        19: goto          44
        22: astore_1
        // 获取System.out标准输出静态属性
        23: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        // 将常量池字符串"catch code"压入操作数栈顶
        26: ldc           #8                  // String catch code
        // 调用println方法
        28: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        31: aload_1
        // 将栈顶的异常对象抛出
        32: athrow
        33: astore_2
        // 获取System.out标准输出静态属性
        34: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        // 将常量池字符串"finally code"压入操作数栈顶
        37: ldc           #6                  // String finally code
        // 调用println方法
        39: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        42: aload_2
        // 将栈顶的异常对象抛出
        43: athrow
        // 正常执行返回
        44: return
      Exception table:
         from    to  target type
             0    11    22   Class java/lang/RuntimeException
             0    11    33   any
            22    34    33   any
      ...
}

通过阅读上面的字节码内容可以知道:

finally中的语句块会以“内联”的方式拼接到try后面(其实catch不再次抛出也会拼接,可以注释掉源码中catch语句块中的throw ex再生成字节码观察一下)。

如果try中抛出了异常如何处理呢?

测试源码中try{}里面的testMethodThrowException()方法抛出了一个RuntimeException异常。 那在字节码层面如何执行呢?

先认识一下Exception table

Exception table可以理解成一张表, 表中有如下属性:

{
 from 字节码起始索引
 to 字节码结束索引(不包含)
 target 异常处理字节码索引起始位置
 type 异常类型(any表示任何类型)
}

当触发异常时,会在从上到下遍历Exception table每一条,当触发异常的字节码索引在from-to的范围,抛出的异常type也能匹配则通过target进行处理。否则继续往下寻找,没有找到将抛出到上一层方法的stack frame重复执行此过程。

看看Exception table中的内容:
Exception table:
from    to  target type
 0    11    22   Class java/lang/RuntimeException
 0    11    33   any
22    34    33   any
  1. 字节码索引[0-11(不包括11)]类型为RuntimeException的从target字节码索引22行开始处理。
  2. 字节码索引[0-11(不包括11)]类型为any(任意种类)的从target字节码索引33行开始处理。
  3. 字节码索引[22-34(不包括34)]类型为any(任意种类)的从target字节码索引33行开始处理。
再来分析测试代码异常抛出的执行流程:
  1. 首先在try{}中调用的testMethodThrowException()抛出了异常,对应字节码索引[0-11(不包括11)]将从target字节码索引22行开始处理。
  2. 在字节码索引[22-32]我们先输出了catch code字符串,然后又throw ex(athrow)抛出异常。 此时被Exception table第三行所包含匹配进行处理。
  3. 而Exception table第三行从字节码索引处理开始处理,输出finally code字符串后再次通过athrow抛出异常。后面没有匹配的异常处理条目,将往上一层stack frame抛出(我们是在man方法中,将直接被虚拟机打印到控制台)。

jdk7之后的语法糖

jdk7之后对try/catch做了一些调整,下面是一段测试代码:

public class TestTryJdk7 {

    static class Person implements AutoCloseable{

        @Override
        public void close() {
            System.out.println("Person close");
        }
    }

    public static void main(String[] args) {

        try (Person p = new Person()){
            System.out.println("try catch");
        } catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
            System.out.println("catch code");
            ex.printStackTrace();
        }
    }

}

主要有两点:

  1. try(...){} 只要()中声明的对象实现了AutoCloseable接口,不需要再编写finally语句块手动关闭。
  2. catch(XxException1 | XxException2 e) 语句块()中可以通过“|”处理多个异常。

有兴趣的同学也可以翻译成字节码查看一下是如何处理的。

总结

通过try/catch/finally语法语义也可以理解执行流程, 但结合字节码分析会有更深层次的理解(也不再担心面试题中各种形形色色的写法)。

思考

不知道大家有没有想过jdk7之后为什么会在try后面增加资源清理支持,难道仅仅是为了编写上面更优雅吗?