try-catch-finally 中的 return

Jul 5, 2016


前几天去面试,出了这么道笔试题

try {
    System.out.println("hello");
    System.exit(0);
} finally {
    System.out.println(" world");
}

我一看就方了:这 finally 还执行不?

答案是不执行的,因为虚拟机停了全部都停了。 当然我选错了。

然后我又想起了很早之前做过的题

try {
    System.out.println("try");
    if (Math.random() < 0.5) throw new Exception();
    return 10;
} catch (Exception e) {
    System.out.println("catch");
    return 9;
} finally {
    return 20;
}

问最后的返回结果是多少。

可能大家都知道最后返回的是 20,那么一开始的 10 去哪里了呢?

反编译下。

{
  public Hello();
    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 18: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: invokestatic  #3                  // Method test:()I
         6: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 20: 0
        line 21: 9

  public static int test();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String try
         5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: invokestatic  #7                  // Method java/lang/Math.random:()D
        11: ldc2_w        #8                  // double 0.5d
        14: dcmpg
        15: ifge          26
        18: new           #10                 // class java/lang/Exception
        21: dup
        22: invokespecial #11                 // Method java/lang/Exception."<init>":()V
        25: athrow
        26: bipush        10
        28: istore_0
        29: bipush        20
        31: ireturn
        32: astore_0
        33: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        36: ldc           #12                 // String catch
        38: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        41: bipush        9
        43: istore_1
        44: bipush        20
        46: ireturn
        47: astore_2
        48: bipush        20
        50: ireturn
      Exception table:
         from    to  target type
             0    29    32   Class java/lang/Exception
             0    29    47   any
            32    44    47   any
      LineNumberTable:
        line 25: 0
        line 26: 8
        line 27: 26
        line 32: 29
        line 28: 32
        line 29: 33
        line 30: 41
        line 32: 44
      StackMapTable: number_of_entries = 3
        frame_type = 26 /* same */
        frame_type = 69 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 78 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
}

那么问题就来了,这里的反编译代码和 Java Virtual Machine Specification 上的不一样。 查了一下,原来虚拟机说明上的是 50.0 以下的版本,而我的是 52。这就非常地尴尬了。

新版本的虚拟机已经取消了 jsr 这个指令,不知道是什么原理让执行 try 的时候不执行 ireturn. 不过这也不影响,我们能看出在 try 块 return 值的时候,这个值被 store 在了 local variable 里,然后跑去执行 finally 里的内容了。而在 finally 处,一个 bipush,紧接着就是 ireturn,这时候整个 frame 被销毁,因此刚才存的变量完全没用了。

如果 finally 里没有返回值,在 finally 处会 load_0,然后 ireturn.

到此为止原理应该是清楚了,如果想要弄清是怎么跳转到 finally 处指令的大概要等到下一版 JVM Specification 了。

今天看了这么多才发现对 Java 里栈的理解完全错了。每个线程在启动的时候分配一个栈,然后 Operand Stack 和 local variable 都分配在 frame 上,这是调用每个方法时产生的!