在一些传统的编程语言,如C语言中,并无专门处理异常的机制,程序员一般用方法的特定返回值来表示异常状况,而且程序的正常流程和异常流程都采用一样的流程控制语句。
Java语言按照面向对象的思想来处理异常,使得程序具备更好的可维护性。Java异常处理机制具备如下优势:html
把各类不一样类型的异常状况进行分类,用Java类来表示异常状况,这种类被称为异常类。把异常状况表示成异常类,能够充分发挥类的可扩展和可重用的优点。java
异常流程的代码和正常流程的代码分离,提升了程序的可读性,简化程序的结构。程序员
能够灵活的处理异常,若是当前方法有能力处理异常,就捕获并处理它,不然只须要抛出异常,由方法调用。编程
关于异常的使用我就再也不多说了,在这里仍是先提几个问题:数组
catch多个异常的时候,按什么规则选择呢架构
throws异常是不是函数签名的一部分呢jvm
覆盖父类的带throws的函数是否也须要加throws呢编程语言
同时实现多个接口中同名抛出异常的函数最后抛出异常的集合是什么呢函数
接下来咱们回答其中的部分问题,先看一个例子spa
能够看到Java是按照catch声明的顺序来捕获异常的,且编译器不容许将父类异常声明在子类以前。
throws异常显然不是函数的一部分,由于两个throws不一样的同名同参数的函数不容许重载。
从上图咱们能够看出覆盖对抛出异常的声明并无要求。
上图能够看出编译器对接口的方法实现也并没有什么要求,重点在于try-catch块的检查,你不能catch一个你在throw块里不可能抛出的检查类型异常,而这种判断是经过你调用方法声明的抛出异常,即便你在方法实现里不可能抛出该异常,你加在throws里,同样能够蒙骗编译器。对于方法声明的抛出异常,只有一个条件须要知足,那就是你的实现中可能抛出的检查类型异常要么处理要么声明抛出,不须要考虑继承和实现关系给throws带来的影响,这是参考文章中的一点小错误,特此更正。
Throwable
Throwable是 Java 语言中全部错误或异常的超类。
Throwable包含两个子类: Error 和 Exception。它们一般用于指示发生了异常状况。
Throwable包含了其线程建立时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。
Exception
Exception及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。
RuntimeException
RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
编译器不会检查RuntimeException异常。例如,除数为零时,抛出ArithmeticException异常。RuntimeException是ArithmeticException的超类。当代码发生除数为零的状况时,假若既"没有经过throws声明抛出ArithmeticException异常",也"没有经过try...catch...处理该异常",也能经过编译。这就是咱们所说的"编译器不会检查RuntimeException异常"!
若是代码会产生RuntimeException异常,则须要经过修改代码进行避免。例如,若会发生除数为零的状况,则须要经过代码避免该状况的发生!
Error
和Exception同样,Error也是Throwable的子类。它用于指示合理的应用程序不该该试图捕获的严重问题,大多数这样的错误都是异常条件。
和RuntimeException同样,编译器也不会检查Error。
Java将可抛出(Throwable)的结构分为三种类型:被检查的异常(Checked Exception),运行时异常(RuntimeException)和错误(Error)。
(01) 运行时异常
定义: RuntimeException及其子类都被称为运行时异常。
特色: Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,假若既"没有经过throws声明抛出它",也"没有用try-catch语句捕获它",仍是会编译经过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。
虽然Java编译器不会检查运行时异常,可是咱们也能够经过throws进行声明抛出,也能够经过try-catch对它进行捕获处理。
若是产生运行时异常,则须要经过修改代码来进行避免。例如,若会发生除数为零的状况,则须要经过代码避免该状况的发生!
(02) 被检查的异常
定义: Exception类自己,以及Exception的子类中除了"运行时异常"以外的其它子类都属于被检查异常。
特色: Java编译器会检查它。此类异常,要么经过throws进行声明抛出,要么经过try-catch进行捕获处理,不然不能经过编译。例如,CloneNotSupportedException就属于被检查异常。当经过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。
被检查异常一般都是能够恢复的。
(03) 错误
定义: Error类及其子类。
特色: 和运行时异常同样,编译器也不会对错误进行检查。
当资源不足、约束失败、或是其它程序没法继续运行的条件发生时,就产生错误。程序自己没法修复这些错误的。例如,VirtualMachineError就属于错误。
按照Java惯例,咱们是不该该是实现任何新的Error子类的!
首先介绍下java的异常表(Exception table),异常表是JVM处理异常的关键点,在java类中的每一个方法中,会为全部的try-catch语句,生成一张异常表,存放在字节码的最后,该表记录了该方法内每一个异常发生的起止指令和处理指令。
接下来看一个例子:
public void catchException() { long l = System.nanoTime(); for (int i = 0; i < testTimes; i++) { try { throw new Exception(); } catch (Exception e) { //nothing to do } } System.out.println("抛出并捕获异常:" + (System.nanoTime() - l)); }
字节码以下
面请结合java代码和生成的字节码来看下面的指令分析:
0-4号: 执行try前面的语句
5号: 执行try语句前保存现场
6号: 执行try语句后跳转指令行,图中表示跳转到22
9-17号: try-catch代码生成指令,结合红色框图异常表,表示9-17号指令如有Exception异常抛出就执行17行指令.
16号: athrow 表示抛出异常
17号: astore 表示jvm将该异常实例存储到局部变量表中方便一旦出方法栈调用方能够找到
22号: 恢复try语句执行前保存的现场
对比指令分析,再结合使用try-catch代码分析:
若try没有抛出异常,则继续执行完try语句,跳过catch语句,此时就是从指令6跳转到指令22.
若try语句抛出异常则执行指令17,将异常保存起来,若异常被方法抛出,调用方拿到异常可用于异常层次索引。
经过以上的分析,能够知道JVM是怎么捕获并处理异常,其实就是使用goto指令来作上下文切换。
上面大体介绍了异常是如何产生并捕获的,接下来咱们详细讲讲athrow指令抛出异常后的故事,也就是如何处理异常的问题。
athrow指令,这个指令运做过程大体是首先检查操做栈顶,这时栈顶必须存在一个reference类型的值,而且是java.lang.Throwable的子类(虚拟机规范中要求若是遇到null则看成NPE异常使用),而后暂时先把这个引用出栈,接着搜索本方法的异常表,找一下本方法中是否有能处理这个异常的handler,若是能找到合适的handler就会从新初始化PC寄存器指针指向此异常handler的第一个指令的偏移地址。接着把当前栈帧的操做栈清空,再把刚刚出栈的引用从新入栈。若是在当前方法中很悲剧的找不到handler,那只好把当前方法的栈帧出栈(这个栈是VM栈,不要和前面的操做栈搞混了,栈帧出栈就意味着当前方法退出),这个方法的调用者的栈帧就天然在这条线程VM栈的栈顶了,而后再对这个新的当前方法再作一次刚才作过的异常handler搜索,若是仍是找不到,继续把这个栈帧踢掉,这样一直到找,要么找到一个能使用的handler,转到这个handler的第一条指令开始继续执行,要么把VM栈的栈帧抛光了都没有找到指望的handler,这样的话这条线程就只好被迫终止、退出了。
对于Java语言中的关键字catch和finally,虚拟机中并无特殊的字节码指令去支持它们,都是经过编译器生成字节码片断以及不一样的异常处理器来实现。
咱们总结一下athrow指令中虚拟机可能作的事情:
检查栈顶异常对象类型(只检查是否是null,是否referance类型,是否Throwable的子类通常在类验证阶段的数据流分析中作,或者索性不作靠编译器保证了,编译时写到Code属性的StackMapTable中,在加载时仅作类型验证)
把异常对象的引用出栈
搜索异常表,找到匹配的异常handler
重置PC寄存器状态
清理操做栈
把异常对象的引用入栈
把异常方法的栈帧逐个出栈(这里的栈是VM栈)
残忍地终止掉当前线程。
这里直接给出一些结论吧:
新建一个异常对象比新建一个普通对象在耗时上多一个数量级,抛出并捕获异常的耗时比新建一个异常在耗时上也要多一个数量级。建立一个异常对象倒是要比一个普通对象耗时多,捕获一个异常耗时更甚。捕获的过程咱们上面已经简要介绍了,为何新建一个异常对象这么耗时?且看源码:
在java中,全部的异常都继承自Throwable类,Throwable的构造函数
public Throwable() { ... fillInStackTrace(); ... }
有个nativ方法public synchronized native Throwable fillInStackTrace();这个方法会存入当前线程的堆栈信息。也就是说每次建立一个异常实例都会把堆栈信息存一遍。这就是时间开销的主要来源了。
这个时候咱们能够下一个结论:新建异常对象比建立一个普通对象是要更加的耗时。
能避开建立异常的这个耗时吗?答案是能够的,若是在程序中咱们不关心异常抛出的异常占信息,咱们能够本身定义一个异常继承自已有的异常类型,并写一个方法覆盖掉fillInStackTrace方法就好了。