WhatAKitty Daily

A Programmer's Daily Record

finally-return在JVM级别的解释

WhatAKitty   阅读次数loading...

背景

刚看了极客时间关于JVM异常的相关信息,然后在评论里看到这么一条:

如果finally有return语句,catch内throw的异常会被忽略,这个从jvm层面怎么解释呢?
2018-09-02
 作者回复
catch里抛的异常会被finally捕获了,再执行完finally代码后重新抛出该异常。由于finally代码块有个return语句,在重新抛出前就返回了。

你可以利用这篇文章的知识,就着javap的输出,分析一下具体的程序路径

所以就着JVM字节码具体分析了一下两种程序路径:

  • catch内抛出异常, finally内存在return
  • catch内抛出异常, finally内不存在return

finally内不存在return

先看下正常情况,即:finally内不存在return。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExceptionTest {

public static void main(String[] args) throws Exception {
try {
throw new RuntimeException("runtime exception");
} catch (Exception e) {
throw new Exception("inner exception");
} finally {
System.out.println("finally");
}

}

}

通过javap获得的字节码内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
"C:\Program Files\Java\jdk1.8.0_181\bin\javap.exe" -l -c com.whatakitty.learn.ExceptionTest
Compiled from "ExceptionTest.java"
public class com.whatakitty.learn.ExceptionTest {
public com.whatakitty.learn.ExceptionTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/whatakitty/learn/ExceptionTest;

# 主要查看这一块,这里是main方法的字节码内容
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: new #2 // class java/lang/RuntimeException
3: dup
4: ldc #3 // String runtime exception
6: invokespecial #4 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
# 在这里抛出RuntimeException异常,从异常表内检查to和from的的范围以及对应type的匹配,获得到异常处理的目标为#10
9: athrow
# 抛出异常后再这里开始处理RuntimeException异常
10: astore_1
11: new #5 // class java/lang/Exception
14: dup
15: ldc #6 // String inner exception
17: invokespecial #7 // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
# 在处理异常的时候,又抛出一个新的Exception异常,同样查找异常表,获取到any异常的匹配,目标为#21
20: athrow
# 开始处理未捕获的异常Exception
21: astore_2
22: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
25: ldc #9 // String finally
27: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: aload_2
# 直接将未捕获异常抛出,JVM会在执行完整个方法后,弹出栈帧,然后会处理新的顶层栈帧。这里的话,由于无新的栈帧,程序直接结束
31: athrow
# 异常表,所有的捕获异常都会在这里记录
Exception table:
from to target type
0 10 10 Class java/lang/Exception
0 22 21 any
LineNumberTable:
line 11: 0
line 12: 10
line 13: 11
line 15: 21
line 17: 30
LocalVariableTable:
Start Length Slot Name Signature
11 10 1 e Ljava/lang/Exception;
0 32 0 args [Ljava/lang/String;
}

finally内存在return

查看了不存在return的正常情况,现在看一下如果在finally内直接返回return,字节码会有什么变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExceptionTest {

public static void main(String[] args) throws Exception {
try {
throw new RuntimeException("runtime exception");
} catch (Exception e) {
throw new Exception("inner exception");
} finally {
return;
}

}

}

通过javap获取的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
"C:\Program Files\Java\jdk1.8.0_181\bin\javap.exe" -l -c com.whatakitty.learn.ExceptionTest
Compiled from "ExceptionTest.java"
public class com.whatakitty.learn.ExceptionTest {
public com.whatakitty.learn.ExceptionTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/whatakitty/learn/ExceptionTest;

public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: new #2 // class java/lang/RuntimeException
3: dup
4: ldc #3 // String runtime exception
6: invokespecial #4 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
9: athrow
10: astore_1
11: new #5 // class java/lang/Exception
14: dup
15: ldc #6 // String inner exception
17: invokespecial #7 // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
# 截止到获取异常并抛出新的Exception异常为止,与前者的流程保持一致。
20: athrow
# 根据异常表定位并获取目标#21后开始执行finally块的代码
21: astore_2
# 由于在finally块内直接返回了return,并不会再执行athrow的字节码指令,也就是说:
# 内部抛出的Exception异常,已经被JVM忽略
22: return
Exception table:
from to target type
0 10 10 Class java/lang/Exception
0 22 21 any
LineNumberTable:
line 11: 0
line 12: 10
line 13: 11
line 15: 21
LocalVariableTable:
Start Length Slot Name Signature
11 10 1 e Ljava/lang/Exception;
0 23 0 args [Ljava/lang/String;
}

总结

按照正常的顺序逻辑,如果出现未捕获异常,则会在finally后执行完成后抛出这个异常。但是,如果在finally块内直接返回,则程序无法执行到抛出异常这步操作就已经被返回了。