异常处理的组成要素
异常处理的两大组成要素是抛出异常和捕获异常。这两大要素共同实现程序控制流的非正常转移。
抛出异常
抛出异常可分为显示和隐式两种。
- 显示抛异常的主体是应用程序,指的是在程序中使用”throw”关键字,手动将异常实例抛出。
- 隐式抛异常的主体则是JVM,指的是JVM在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。
捕获异常
捕获异常涉及三种代码块。
- try代码块:用来标记需要进行异常监控的代码。
- catch代码块:跟在try代码块之后,用来捕获在try代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch代码块还定义了针对该异常类型的异常处理器。在Java中,try代码块后面可以跟着多个catch代码块,来捕获不同类型的异常。JVM会从上至下匹配异常处理器。前面的catch代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。
- finally代码块:跟在try代码块和catch代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源。
在程序正常执行的情况下,这段代码会在try代码块之后运行。否则try代码块触发异常的情况下,如果该异常没有被捕获,finally代码块会直接运行,并且在运行之后重新抛出该异常。
如果该异常被catch代码块捕获,finally代码块则在catch代码块之后运行。在某些不幸的情况下,catch代码块也触发了异常,那么finally代码块同样会运行,并会抛出catch代码块触发的异常。咋某些极端不幸的情况下,finally代码块也触发了异常,那么只好终端当前finally代码块的执行,并往外抛异常。
异常的基本概念
在Java语言规范中,所有异常都是Throwable类或者其子类的实例。Throwable有两大直接子类。
- 第一个是Error,涵盖程序不应捕获的异常。当程序触发Error时,它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。
- 第二子类是Exception,涵盖程序可能需要捕获并且处理的异常。
Exception有一个特殊的子类RuntimeException,用来 表示”程序虽然无法继续执行,但是还能抢救一下”的情况。
RuntimeException和Error属于Java里的非检查异常(unchecked exception)。在Java语法中,所有的检查异常都需要程序显式的捕获,或者在方法声明中用throws关键字标注。通常情况下,程序中自定义的异常应为检查异常,以便最大化利用Java编译器的编译时检查。
异常实例的构造十分昂贵。这是由于在构造异常实例时,JVM需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的Java栈帧,并记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。
实践中,抛出新建异常实例。而不是缓存异常实例,因为栈轨迹不一致。
在生成栈轨迹时,JVM会忽略掉异常构造器以及填充栈帧的Java方法(Throwable.fillnStackTrace),直接从新建异常位置开始算起。持外,JVM虚拟机还会忽略标记为不可见的Java方法栈帧。
Java虚拟机是如何捕获异常的
在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由from指针、to指针、target指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index, bci),用以定位字节码。
1 |
|
其中from指针和to指针标识了该异常处理器所监控的范围,try代码块索覆盖的范围。target指针则指向异常处理器的起始位置,catch代码块的起始位置。
当程序触发异常时,JVM会从上到下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,JVM会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,JVM会将控制流转移至该条目target指针指向的字节码。
如果遍历完所有异常条目,JVM仍未匹配到异常处理器,那么它会弹出当前方法对应的Java栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,JVM需要遍历当前线程Java栈上所有方法的异常表。
finally代码块的编译比较复杂。当前版本Java编译器的做法,是复制finally代码块的内容,分别放在try-catch代码块所有正常执行路径以及异常执行路径的出口中。
针对异常执行路径,Java编译器会生成一个或多个异常表条目,监控整个try-catch代码块,并且捕获所有种类的异常(在javap中以any指代)。这些异常表条目的target指针将各自指向另一份复制的finally代码块。并且,在这个finally代码块的最后,若触发新异常,Java编译器会重新抛出所捕获的异常。
最后一份finally代码块作为异常处理器,监控try代码块以及catch代码块。它将捕获try代码块触发的、未被catch代码块捕获的异常,以及catch代码块触发的异常。(如果catch代码块捕获了异常,并且触发了另一个异常,那么finally捕获并且重抛的异常是最后的异常,原本的异常便会被忽略掉!)
Java7的Suppressed异常以及语法糖(try-with-resources、多异常捕获)
Java7引入Suppressded异常允许开发人员将一个异常附于另一个异常之上。抛出的异常可以附带多个异常的信息(Java层面的finally代码块缺少指向所捕获异常的引用,使用Suppressed的特性较繁琐)。
Java7构造了名为try-with-resources的语法糖,在字节码层面自动使用Suppressed异常。精简资源打开关闭的用法。
Java7之前,对于打开的资源,需要定义一个finally代码块,来确保资源在正常或者异常执行状况下都能关闭。资源的关闭操作本身容易触发异常。如果同时打开多个资源,那么每一个资源都要对应一个独立的try-finally代码块,以保证每个资源都能够关闭。代码将会变得十分繁琐。
try-with-resources语法糖,简化了上述操作。程序可以在try关键字后声明并实例化实现了AutoCloseable接口的类,编译器将自动添加对应的close()操作。与手工代码相比,try-with-resources还会使用Suppressed异常的功能,来避免原异常”被消失”。
除了try-with-resources语法糖之外,Java7还支持在同一catch代码块中捕获多种异常。生成多个异常表条目,多异常捕获语法糖。