异常处理的两大组成要素:抛出异常和捕获异常。这两大要素共同实现程序控制流的非正常转移。
抛出异常分为:显式和隐式两种。
显式抛异常的主题是应用程序,它指的是在程序中使用 “throw” 关键字。手动将异常实例抛出。
隐式抛异常的主题是java虚拟机,它指的是java虚拟机在执行过程中,碰到无法继续执行的异常状态,自动过抛出异常。举例来说,java虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException)。
捕获异常则涉及了如下三种代码块:
1、try代码块:用来标记需要进行异常监控的代码。
2、catch代码块:跟在try代码块之后,用来捕获在try代码块中触发的某种类型的异常。除了声明所捕获异常的类型之外,catch代码块还定义了针对该异常类型的异常处理器。在java中try代码块后可以跟多个catch代码块,来捕获不同的异常。java虚拟机会从上至下匹配异常处理器。因此,前面的catch代码块所捕获的异常类型不能覆盖后面的,否则编译器会报错。
3、finally代码块:跟在try代码块和catch代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码。例如关闭已打开的系统资源。
在程序正常执行的情况下,这段代码会在try代码块执行之后执行。否则,也就是在try代码块抛异常的情况下,如果该异常没有被捕获,finally代码块会直接运行,并且在运行之后重新抛出异常。
如果该异常被catch代码块捕获,finally代码块则在catch代码块之后运行。在某些不幸的情况下,catch代码块也触发了异常,那么finally代码块同样会执行,并会抛出catch代码块触发的异常。在某极端不幸的情况下,finally代码块也触发了异常,那么只好中断当前finally代码块的执行,并往外抛出异常。
异常的基本概念
在java语言规范中所有的异常都是Throwable类或者其子类实例。Throwable类有两大直接的子类。Error和Exception。
Error:涵盖程序不应捕获的异常。当程序触发error是他的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。
Exception:涵盖程序可能需要捕获并且处理的异常。
Exception 有一个特殊的子类RuntimeException ,用来表示线程虽然无法继续执行但仍可以抢救一下的情况。数组索引越界便是其中的一种。
RuntimeException 和 Error属于java的非检查异常(unchecked exception)。其他异常则属于检查异常(checked exception)。在java语法中所有的检查异常都需要程序显示的捕获,或者在方法声明中用throws关键字标注。通常情况下,程序中自定义的异常为检查异常。以便最大化利用java编译器的编译时检查。
异常实例的构造非常昂贵。这是由于在构造异常实例时,java虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的java栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。
JVM的捕获异常机制
在编译生成的字节码中,每个方法都附带一个异常表。异常表中,每一个条目代表一个异常处理器,并且由from指针、to指针和target指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci)用以定位字节码。其中feom指针和to指针标示了该异常处理器所监控的范围,例如try代码块所覆盖的范围。target指针则指向异常处理器的起始位置,例如catch代码块的起始位置。
当程序触发异常时,JVM会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,JVM会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,JVM会将控制流转移至该条目target指针指向的字节码。如果遍历完所有的条目,JVM仍未匹配到异常处理器,那么它会弹出当前方法所对应的java栈帧,并且在调用者(caller)中重复上述操作。在最坏的情况下,JVM需要遍历当前线程java栈上所有方法的异常表。
finally代码块的编译比较复杂。当前版本java编译器的做法,是复制finally代码块的内容,分别放在try-catch代码块所有正常执行路径以及异常执行路径的出口中。针对异常执行路径,java编译器会生成一个或多个异常表条目,监控整个try-catch代码块,并且捕获所有种类的异常。这些异常表条目的target指针将指向将指向另一份复制的finally代码块。并且,在这个finally代码块的最后,java编译器会重新抛出所捕获得异常。