JVM如何保证执行finally块?

22

这个问题关注于JVM如何确保执行finally块(前提是JVM没有崩溃,线程没有被中断或退出)。

受面试问题启发,我正在尝试了解JVM如何确保在奇怪的情况下执行finally块… 考虑以下代码:

try{

    int[] someArray = new int[10];
    int invalid = someArray[10];
}
catch(IndexOutOfBoundsException e){

    throw new RuntimeException("Other Exception");
}
finally{

    //close open files or HTTP connections etc.
}


即使未显式处理 Other Exception ,该finally块仍然保证会被执行。JVM如何处理这种情况?

我的想法:

据我了解和目前阅读的内容,当遇到未处理的异常时,控制权将从当前线程转移(我认为是到该线程的 ThreadGroup )。 ThreadGroup 中是否有检查需要执行的finally块的规定?我唯一能想到的是可能在某处存储了finally块的地址。然后当检测到异常时,JVM会进行跳转,并在finally块完成执行后返回到异常。

是否有人可以澄清这个实际发生的过程?


我本以为finally块会在通知ThreadGroup之前被处理... - MadProgrammer
1
直到异常一路上传并通过所有异常处理程序,它才被视为“未处理”。 - Hot Licks
2个回答

20

编译这个小程序(我意识到我应该使用你的例子,但这没有任何影响)

public static void main(String[] args) {
    try {
        Float s = Float.parseFloat("0.0327f");
    } finally {
        System.out.println("hello");
    }
}

我使用过

>java -version 
java version "1.8.0-ea"  // should be same for 7
Java(TM) SE Runtime Environment (build 1.8.0-ea-b118)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b60, mixed mode)

然后执行

javac -v -c <fully qualified class name>

获取字节码。你会看到类似这样的内容。

public static void main(java.lang.String[]);
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=3, args_size=1
       0: ldc           #2                  // String 0.0327f
       2: invokestatic  #3                  // Method java/lang/Float.parseFloat:(Ljava/lang/String;)F
       5: invokestatic  #4                  // Method java/lang/Float.valueOf:(F)Ljava/lang/Float;
       8: astore_1
       9: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #6                  // String hello
      14: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: goto          31
      20: astore_2
      21: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      24: ldc           #6                  // String hello
      26: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      29: aload_2
      30: athrow
      31: return
    Exception table:
       from    to  target type
           0     9    20   any
          20    21    20   any
    LineNumberTable:
      line 10: 0
      line 12: 9
      line 13: 17
      line 12: 20
      line 14: 31
    StackMapTable: number_of_entries = 2
         frame_type = 84 /* same_locals_1_stack_item */
        stack = [ class java/lang/Throwable ]
         frame_type = 10 /* same */

你会注意到在finally中的代码出现了两次,一次是在goto之前,一次是之后。你还会注意到Exception table,它指定了如果在某行发生异常要去哪个语句。

因此,如果在语句0-9之间发生任何异常,则转到第20行并执行finally中的所有内容,在goto之后。如果没有发生异常,则先执行finally,然后执行goto跳过finally之后的部分。

在所有情况下,您都将执行finally块中的代码。

  

其他未明确处理的异常

使用finally块,将创建一个Exception table条目,用于处理任何类型Throwable


这里是字节码指令清单。


哦,那么异常表格显示任何遇到的异常执行什么? - Joel
@Joel 正确。如果你在try-finally中加入一个catch(SomeException e),你会看到针对这个异常的异常表条目,其中会标明它可能发生的位置以及如果发生该异常时执行(跳转)的目标位置。 - Sotirios Delimanolis
@SotiriosDelimanolis,我有一个快速的问题想问您,如果try块中有一个return语句,而finally块也有一个return语句。Java将返回finally块中指定的值。为什么try块的值没有被返回呢? - karmanaut
@karmanaut 我不确定你想要多详细的答案,但基本上,在finally块中的return语句将覆盖先前设置的任何返回值。 - Sotirios Delimanolis
@SotiriosDelimanolis 我认为 return 实际上会将控制权转移到调用函数?所以如果执行 try return,它不会将控制权传回并等待 finally 执行吗? - karmanaut
@karmanaut 请看这里的最后一段落(http://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.17)。 - Sotirios Delimanolis

5

我认为这篇博客清晰地描述了以下内容:

如果一个方法定义了try-catch或try-finally异常处理程序,则将创建一个异常表。该表包含每个异常处理程序或finally块的信息,包括处理程序适用的范围、正在处理的异常类型和处理程序代码的位置。

当抛出异常时,JVM会在当前方法中查找匹配的处理程序,如果没有找到,则该方法突然结束,弹出当前堆栈帧,并在调用方法中重新抛出异常(新的当前帧)。如果在所有帧都弹出之前没有找到异常处理程序,则线程终止。如果异常是在最后一个非守护线程中抛出的,例如如果线程是主线程,则这也会导致JVM本身终止。

最后,异常处理程序匹配所有类型的异常,因此每当抛出异常时总是执行。在没有抛出异常的情况下,finally块仍然在方法末尾执行,这是通过在执行return语句之前立即跳转到finally处理程序代码实现的。


你的报价稍微好一些。 - Makoto

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接