做为一个“有经验”的 Java 工程师,你必定知道什么是try-catch-finally
代码块。可是你知道 JVM 是如何处理异常的吗?今天咱们就来说讲异常在 JVM 中的处理机制,以及字节码中异常表。java
但愿在这以后,不会有人再将下面这张表情包发给你……bash
Talk is cheap, show you my code!
工具
首先我编写了第一段测试代码,这里有一个 try-catch 代码块,每一个代码块中都有一行输出,在 catch 代码块中捕获的是 Exception 异常。测试
public static void main(String[] args) {
try {
System.out.println("enter try block");
} catch (Exception e) {
System.out.println("enter catch block");
}
}
复制代码
而后在命令行中先定位到这个类的字节码文件目录中,执行主方法后敲下javap -c 类名
进行反编译,或者直接在编译器中选择Build Project
,而后打开 jclasslib 工具就能够看到这个类的字节码。ui
我选择了第二个方法,主方法的字节码以下图:spa
能够看到0~3行是 try 代码块中的输出语句,12~17行是 catch 代码块中的输出语句。而后重点来了。插件
第8行的字节码是8 goto 20
,这是什么意思呢?没错,盲猜就能猜到,这个字节码指令就是跳转到第20行的意思。这一行是说,若是 try 代码块中没有出现异常,那么就跳转到第20行,也就是整个方法行完成后 return 了。命令行
这是正常的代码执行流程,那么若是出现异常了,虚拟机是如何知道应该“监控” try 代码块?它又是怎么知道该捕获何种异常呢?code
答案就是——异常表。cdn
在一个类被编译成字节码以后,它的每一个方法中都会有一张异常表。异常表中包含了“监控”的范围,“监控”何种异常以及抛出异常后去哪里处理。好比上述的示例代码,在 jclasslib 中它的异常表以下图。
或者在javap -c
命令下异常表是这样的:
Exception table:
from to target type
0 8 11 Class java/lang/Exception
复制代码
不管是哪一种形式的异常表,咱们能够知道的是,异常表中每一行就表明一个异常处理器。
若是程序触发了异常,Java 虚拟机会按照序号遍历异常表,当触发的异常在这条异常处理器的监控范围内(from 和 to),且异常类型(type)与该异常处理器一致时,Java 虚拟机就会跳转到该异常处理器的起始位置(target)开始执行字节码。
若是程序没有触发异常,那么虚拟机会使用 goto 指令跳过 catch 代码块,执行 finally 语句或者方法返回。
接下来在上述的代码中再加入一个 finally 代码块,而后再次执行反编译的命令看看有什么不同。
// 源代码
public static void main(String[] args) {
try {
// dosomething
System.out.println("enter try block");
} catch (Exception e) {
System.out.println("enter catch block");
} finally {
System.out.println("enter finally block");
}
}
复制代码
// 字节码
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <enter try block>
5 invokevirtual #4 <java/io/PrintStream.println>
8 getstatic #2 <java/lang/System.out>
11 ldc #5 <enter finally block>
13 invokevirtual #4 <java/io/PrintStream.println>
16 goto 50 (+34)
19 astore_1
20 getstatic #2 <java/lang/System.out>
23 ldc #7 <enter catch block>
25 invokevirtual #4 <java/io/PrintStream.println>
28 getstatic #2 <java/lang/System.out>
31 ldc #5 <enter finally block>
33 invokevirtual #4 <java/io/PrintStream.println>
36 goto 50 (+14)
39 astore_2
40 getstatic #2 <java/lang/System.out>
43 ldc #5 <enter finally block>
45 invokevirtual #4 <java/io/PrintStream.println>
48 aload_2
49 athrow
50 return
复制代码
finally 代码块在当前版本(jdk 1.8)的 JVM 中的处理机制是比较特殊的。从上面的字节码中也能够明显看到,只是加了一个 finally 代码块而已,字节码指令增长了不少行。
若是再仔细观察一下,咱们能够发现,在字节码指令中,有三块重复的字节码指令,分别是8~13行、28~33行和40~45行,若是对字节码有些了解的同窗或许已经知道了,这三块重复的字节码就是 finally 代码块对应的代码。
出现三块重复字节码指令的缘由是在 JVM 中,全部异常路径(如try、catch)以及全部正常执行路径的出口都会被附加一份 finally 代码块。也就是说,在上述的示例代码中,try 代码块后面会跟着一份 finally 的代码,catch 代码块后面也是如此,再加上本来正常流程会执行的 finally 代码块,在字节码中一共有三份 finally 代码块代码块。
而针对每一条可能出现的异常的路径,JVM 都会在异常表中多生成一条异常处理器,用来监控整个 try-catch 代码块,同时它会捕获全部种类的异常,而且在执行完 finally 代码块以后会从新抛出刚刚捕获的异常。
上述示例代码的异常表以下
Exception table:
from to target type
0 8 19 Class java/lang/Exception
0 8 39 any
19 28 39 any
复制代码
能够看到与原来相比异常表增长了两条,第2条异常处理器异常监控 try 代码块,第3条异常处理器监控 catch 代码块,若是出现异常则会跳转到第39行的 finally 代码块执行。
这就是 finally 必定会在 try-catch 代码块以后执行的缘由了(某些能中断程序运行的操做除外)。
上文说到虚拟机会对整个 try-catch 代码块生成一个或多个异常处理器,若是在 catch 代码块中抛出了异常,这个异常会被捕获,而且在执行完 finally 代码块以后被从新抛出。
那么在这里有一个额外的问题须要说起,假设在 catch 代码块中抛出了异常 A,当执行 finally 代码块时又抛出了异常 B,那么最后抛出的是什么异常呢?
若是有同窗本身尝试过这个操做,就会知道最后抛出的异常 B。也就是说,在捕获了 catch 代码块中的异常后,若是 finally 代码块中也抛出了异常,那么最终将会抛出 finally 中抛出的异常,而原来 catch 代码块中的异常将会被忽略。
讲完了异常在各个代码块中的状况,接下来再来考虑一下 return 关键字吧,若是 try 或者 catch 中有 return,finally 还会执行吗?若是 finally 中也有 return,那么最终返回的值是什么?为了说明这个问题,我编写了一段测试代码,而后找到它的字节码指令。
public static int get() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3;
}
}
// 字节码指令
0 iconst_1
1 istore_0
2 iconst_3
3 ireturn
4 astore_0
5 iconst_2
6 istore_1
7 iconst_3
8 ireturn
9 astore_2
10 iconst_3
11 ireturn
复制代码
正如上文所述,finally 代码块会在全部正常及异常的路径上都复制一份,在这段字节码中,iconst_3 就是对应着 finally 代码块,共三份,因此即使在 try 或者 catch 代码块中有 return 语句,最终仍是会会执行 finally 代码块中的内容。
也就是说,这个方法最终的返回结果是3。