Java 在代码中通过使用 try{}catch(){}finally{} 块来对异常进行捕获或者处理。但是对于 JVM 来说,是如何处理 try/catch 代码块与异常的呢。上一篇文章值中岛异常信息是存放在属性表集合中的Code属性表里,那么这篇博客就单独讲Code属性表中的exception_table。

在讲之前我们先思考两个问题?
1、为什么捕获异常会较大的性能消耗?
2、为什么finally中的代码会永远执行?
**
接下来会从JVM虚拟机的角度来解答这两个问题。

一、概念

1、JVM是如何捕获异常的?

1、编译而成的字节码中,每个方法都附带一个异常表
2、异常表中每一个条目代表一个异常处理器
3、触发异常时,JVM会遍历异常表,比较触发异常的字节码的索引值是否在异常处理器的from指针到to指针的范围内。
4、范围匹配后,会去比较异常类型和异常处理器中的type是否相同
5、类型匹配后,会跳转到target指针所指向的字节码(catch代码块的开始位置)
6、如果没有匹配到异常处理器,会弹出当前方法对应的Java栈帧,并对调用者重复上述操作。

2、什么是异常表?

1. 每个方法都附带一个异常表
2. 异常表中每一个条目, 就是一个异常处理器
异常表如下:

2.2、JVM 异常表与异常处理原理 - 图1

3、什么是异常处理器?其组成部分有哪些?

1、异常处理器由from指针、to指针、target指针,以及所捕获的异常类型所构成(type)。 2、这些指针的数值就是字节码的索引(bytecode index, bci),可以直接去定位字节码。 3、from指针和to指针,标识了该异常处理器所监控的返回 4、target指针,指向异常处理器的起始位置。如catch代码块的起始位置 5、type:捕获的异常类型,如Exception异常类型,如Exception

4、如果在方法的异常表中没有匹配到异常处理器,会怎么样?

1、会弹出当前方法对应的Java栈帧 2、在调用者上重复异常匹配的流程。 3、最坏情况下,JVM需要编译当前线程Java栈上所有方法的异常表

二、代码演示

1、try-catch

  1. public static void main(String[] args) {
  2. try {
  3. mayThrowException();
  4. } catch (Exception e) {
  5. e.printStackTrace();
  6. }
  7. }

对应得java字节码

  1. // 对应的 Java 字节码
  2. public static void main(java.lang.String[]);
  3. Code:
  4. 0: invokestatic mayThrowException:()V
  5. 3: goto 11
  6. 6: astore_1
  7. 7: aload_1
  8. 8: invokevirtual java.lang.Exception.printStackTrace
  9. 11: return
  10. // 异常表
  11. Exception table:
  12. from to target type
  13. 0 3 6 Class java/lang/Exception // 异常表条目

看下面的Exception table表,来进行分析。

1、from和to: 指是try和catch之间的代码的索引位置。from=0,to=3,代表从字节索引0的位置到3(不包括3)。
2、target : 代表catch后代码运行的起始位置。
3、type : 指的是异常类型,这里是指Exception异常。

当程序触发异常时,java虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常

和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目target 指针指向的字节码。
如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,

Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。

2、try-catch-finally

finally 代码块的编译比较复杂。当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中

实际上 Java 编译后,会在代码后附加异常表的形式来实现 Java 的异常处理及 finally 机制(在 JDK1.4.2之前,javac 编译器使用 jsr 和 ret 指令来实现 finally 语句,但是1.4.2之后自动在每段可能的分支路径后将 finally 语句块内容冗余生成一遍来实现。JDK1.7及之后版本,则完全禁止在 Class 文件中使用 jsr 和 ret 指令)。

2.2、JVM 异常表与异常处理原理 - 图2

代码示例:

  1. public static void XiaoXiao() {
  2. try {
  3. dada();
  4. } catch (Exception e) {
  5. e.printStackTrace();
  6. } finally {
  7. System.out.println("Finally");
  8. }
  9. }

反编译的字节码:

  1. public static void XiaoXiao();
  2. Code:
  3. 0: invokestatic #3 // Method dada:()V
  4. 3: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
  5. 6: ldc #7 // String Finally
  6. 8: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  7. 11: goto 41
  8. 14: astore_0
  9. 15: aload_0
  10. 16: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
  11. 19: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
  12. 22: ldc #7 // String Finally
  13. 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  14. 27: goto 41
  15. 30: astore_1
  16. 31: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
  17. 34: ldc #7 // String Finally
  18. 36: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  19. 39: aload_1
  20. 40: athrow
  21. 41: return
  22. Exception table:
  23. from to target type
  24. 0 3 14 Class java/lang/Exception
  25. 0 3 30 any
  26. 14 19 30 any

和之前有所不同,这次
1、异常表中,有三条数据,而我们仅仅捕获了一个Exception
2、异常表的后两个item的type为any

上面的三条异常表item的意思为
1、如果0到3之间,发生了Exception类型的异常,调用14位置的异常处理者。
2、 如果0到3之间,无论发生什么异常,都调用30位置的处理者。
3、 如果14到19之间(即catch部分),不论发生什么异常,都调用30位置的处理者。

通过上面那幅图和javap反编译代码就可以很好的解释为什么finally里面的代码永远会执行?

原因:因为当前版本Java编译器的做法,是复制finally代码块的内容,分别放到所有正常执行路径,以及异常执行路径的出口中
这三份finally代码块都放在什么位置:
第一份位于try代码后 : 若果try中代码正常执行,没有异常那么finally代码就在这里执行。
第二份位于catch代码后 : 如果try中有异常同时被catch捕获,那么finally代码就在这里执行。
第三份位于异常执行路径** : 如果如果try中有异常但没有被catch捕获,或者catch又抛异常,那么就执行最终的finally代码。