×

从字节码看try catch finally的return如何执行

96
lanzry
2018.01.30 17:36* 字数 1803

测试代码很简单,如下:
Test.java

public class Test {
    public int get() {
        try{
            return 0;
        } catch (Exception e) {
            e.printStackTrace();
            return 1;
        } finally {
            return 2;
        }
    }
}

尽量简单的代码,用以说明问题。

javac Test.java

编译后产生Test.class,打开

cafe babe 0000 0034 0018 0a00 0500 1107
0012 0a00 0200 1307 0014 0700 1501 0006
3c69 6e69 743e 0100 0328 2956 0100 0443
6f64 6501 000f 4c69 6e65 4e75 6d62 6572
5461 626c 6501 0003 6765 7401 0003 2829
4901 000d 5374 6163 6b4d 6170 5461 626c
6507 0012 0700 1601 000a 536f 7572 6365
4669 6c65 0100 0954 6573 742e 6a61 7661
0c00 0600 0701 0013 6a61 7661 2f6c 616e
672f 4578 6365 7074 696f 6e0c 0017 0007
0100 0454 6573 7401 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 136a 6176
612f 6c61 6e67 2f54 6872 6f77 6162 6c65
0100 0f70 7269 6e74 5374 6163 6b54 7261
6365 0021 0004 0005 0000 0000 0002 0001
0006 0007 0001 0008 0000 001d 0001 0001
0000 0005 2ab7 0001 b100 0000 0100 0900
0000 0600 0100 0000 0100 0100 0a00 0b00
0100 0800 0000 6400 0100 0400 0000 1003
3c05 ac4c 2bb6 0003 043d 05ac 4e05 ac00
0300 0000 0200 0400 0200 0000 0200 0d00
0000 0400 0b00 0d00 0000 0200 0900 0000
1a00 0600 0000 0400 0200 0900 0400 0500
0500 0600 0900 0700 0b00 0900 0c00 0000
0a00 0244 0700 0d48 0700 0e00 0100 0f00
0000 0200 10

cafe babe这样还是比较难懂的,我们当然也可以强行自己去解释过来。但是有更方便的方法:

javap -verbose Test.class

利用javap工具帮助解释一下字节码,显示如下:

$ javap -verbose Test.class
Classfile /E:/workspace/java/Test.class
  Last modified 2018-1-29; size 405 bytes
  MD5 checksum f8f6002de3931b2e95125679f2ce1f6c
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // java/lang/Exception
   #3 = Methodref          #2.#19         // java/lang/Exception.printStackTrace                          :()V
   #4 = Class              #20            // Test
   #5 = Class              #21            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               get
  #11 = Utf8               ()I
  #12 = Utf8               StackMapTable
  #13 = Class              #18            // java/lang/Exception
  #14 = Class              #22            // java/lang/Throwable
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #6:#7          // "<init>":()V
  #18 = Utf8               java/lang/Exception
  #19 = NameAndType        #23:#7         // printStackTrace:()V
  #20 = Utf8               Test
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/Throwable
  #23 = Utf8               printStackTrace
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>                          ":()V
         4: return
      LineNumberTable:
        line 1: 0

  public int get();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_2
         3: ireturn
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Exception.prin                          tStackTrace:()V
         9: iconst_1
        10: istore_2
        11: iconst_2
        12: ireturn
        13: astore_3
        14: iconst_2
        15: ireturn
      Exception table:
         from    to  target type
             0     2     4   Class java/lang/Exception
             0     2    13   any
             4    11    13   any
      LineNumberTable:
        line 4: 0
        line 9: 2
        line 5: 4
        line 6: 5
        line 7: 9
        line 9: 11
      StackMapTable: number_of_entries = 2
        frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
}
SourceFile: "Test.java"

这样看就清晰很多,minor version 0、major version 52是支持的JDK版本,也就是JDK1.8,Constant pool是著名的常量池,还有访问权限public = ACC_PUBLIC。
本文重点不是这些,重点看get()方法的字节码。

descriptor: ()I

表示没有参数,返回值是int型。

flags: ACC_PUBLIC

表示public方法。重点看Code的部分:

    Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_2
         3: ireturn
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Exception.prin                          tStackTrace:()V
         9: iconst_1
        10: istore_2
        11: iconst_2
        12: ireturn
        13: astore_3
        14: iconst_2
        15: ireturn
      Exception table:
         from    to  target type
             0     2     4   Class java/lang/Exception
             0     2    13   any
             4    11    13   any
      LineNumberTable:
        line 4: 0
        line 9: 2
        line 5: 4
        line 6: 5
        line 7: 9
        line 9: 11
      StackMapTable: number_of_entries = 2
        frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]

分析一下其中的附加属性:

stack=1, locals=4, args_size=1

我们知道一个方法在虚拟机中对应一个栈帧,一个栈帧内包含了操作数栈、局部变量表等。

  1. stack=1就表示着操作数栈的最大深度是1。

  2. locals表示局部变量表需要的存储空间,单位是Slot,Slot是虚拟机为局部变量表分配内存使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数、显示异常处理器的参数(包括实例方法中的隐藏参数“this”)、方法体中定义的局部变量都需要使用局部变量表来存放。

依以上观点,this算一个,i算一个,Exception e算一个,但locals=4?
稍后我们一起看字节码指令再来揭晓。

  1. args_size=1,表示方法参数数量是1。get()方法明明没有参数,这1,就是前文提到的“实例方法中隐藏参数this”。在任何实例方法里面,都可以通过this关键字访问到此方法所属对象。实现非常简单:仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通参数的访问,任何在虚拟机调用此方法时自动传入此参数而已。

static修饰的静态方法属于类方法,就不存在此参数了,静态方法的args_size和locals就会从0开始计数。

Code的尾部跟着3个标签:Exception table、LineNumberTable、StackMapTable。接下来理解一下:

  1. Exception table
      Exception table:
         from    to  target type
             0     2     4   Class java/lang/Exception
             0     2    13   any
             4    11    13   any

JVM8虚拟机规范【https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.3】中的Code属性的标准结构如下:

    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];

看代码就已经比较好理解了:从start_pc(开始的pc指针)执行到end_pc(结束的pc指针),假如发生了catch_type类型的异常,就跳转到异常处理的pc指针处执行(handler_pc)
从0到2执行,要是有java/lang/Exception异常,就跳转到target(目标)4行执行;假如是其他的异常(any),就跳到13行执行;同时发生java/lang/Exception异常后,执行4-11行时假如又发生任意异常(any),就跳到13行执行。
jvm虚拟机就是这样通过异常表来执行的。具体我们待会再进一步到Code中看怎么执行。

  1. LineNumberTable

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系。它并不是 运行时必须的属性,但默认会生成到Class文件之中,可以在javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

  1. StackMapTable
    StackMapTable和Class文件的字节码合法性验证相关,是JDK1.7之后不可或缺的一个属性。《Java虚拟机规范(Java SE 7版)》花费了整整120页来讲解描述,但与本文内容无关,略去不讲。

下面一起来看重点部分:代码执行

         0: iconst_0
         1: istore_1
         2: iconst_2
         3: ireturn
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Exception.printStackTrace:()V
         9: iconst_1
        10: istore_2
        11: iconst_2
        12: ireturn
        13: astore_3
        14: iconst_2
        15: ireturn
      Exception table:
         from    to  target type
             0     2     4   Class java/lang/Exception
             0     2    13   any
             4    11    13   any

最好是结合原本代码一起看

public class Test {
    public int get() {
        try{
            return 0;
        } catch (Exception e) {
            e.printStackTrace();
            return 1;
        } finally {
            return 2;
        }
    }
}

0: iconst_0
把第0个int型也就是0推送到操作数栈栈顶。


1: istore_1
那意思就是把栈顶的0存入局部变量表的位置1,为什么不是从0开始?(提示“this”)

2: iconst_2
按理说try已经走完了,就一个return 0,应该return才对。但是没有,第二个int型数据,不就是finally里面的2,把finally里面的2推到了栈顶。所以知道为什么把0存入局部变量表了,因为虚拟机分配的操作数栈深度是1!

3: ireturn
返回int型,操作数栈栈顶的数据,也就是2。所以说明try里、finally都有return时,执行finally里的return。


正常执行的部分已经看完了。异常的部分,使用异常表进行处理,具体执行逻辑前面已经讨论过,这里我们再来顺着逻辑梳理一遍。
从异常表我们知道,0-2是正常执行(3就return了,所以不算在里面),假如有Exception类型的异常,就跳转到4执行。接下来从4行开始看。
4: astore_1
上面我们知道,曾经把0存入局部变量表的位置1。这里是因为0的作用域是try对应的大括号,catch里面0已经无效,所以局部变量表也就不再为它保持内存。这个栈顶引用类型也就是抛出来的异常Exception e了。

5: aload_1
把刚刚存入局部变量表的引用类型e又加载到操作数栈栈顶,因为有一行:e.printStackTrace();需要用到。

6: invokevirtual #3 // Method java/lang/Exception.printStackTrace:()V
调用实例方法,占据了三行。

9: iconst_1
直接9行,以第0行类推,就是把代码中第1个int型也就是catch的1推到操作数栈栈顶,也就是1。
10: istore_2
以1行类推,也就是把栈顶的1存到局部变量表位置2(位置0是this,位置1是对象e)
11: iconst_2
把第二个int型finally的2推送到栈顶
12: ireturn
return掉栈顶的数据(也就是finally的2)


由此可见,即使catch中有return,执行的也是finally中的return。

由异常表中,我们知道,编译器编译过后,又考虑了抛出的异常不是Exception类型的情况,无论try或catch出异常时,都去执行13行的代码。
13: astore_3
类比5行,这里是把不明类型的异常存入局部变量表第3个位置。这个异常时可能在catch中出现的,catch中我们占用了局部变量表位置1和2,所以用3是最保险的。
14: iconst_2
依旧是把finally的2推到栈顶
15: ireturn
把栈顶的2return出去

至此,我们就弄明白了try catch finally中都有return的情况下,虚拟机会如何去执行return代码。那就是最终只走finally的return。






JVM
Web note ad 1