JVM的异常体系
任何程序都追求正确有效的运行,除了保证我们代码尽可能的少出错之外,我们还要考虑如何有效的处理异常,一个良好的异常框架对于系统来说是至关重要的。最近在采集框架的时候系统的了解一边,收获颇多,特此记录相关的理论。
1 .异常体系简介:
异常是指由于各种不期而至的情况,导致程序中断运行的一种指令流,如:文件找不到、非法参数、网络超时等。为了保证正序正常运行,在设计程序时必须考虑到各种异常情况,并正确的对异常进行处理。异常也是一种对象,java当中定义了许多异常类,并且定义了基类java.lang.Throwable作为所有异常的超类。Java语言设计者将异常划分为两类:Error和Exception,其体系结构大致如下图所示:
Throwable:有两个重要的子类:Exception(异常)和Error(错误),两者都包含了大量的异常处理类。
1、Error(错误):
是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如说当jvm耗完可用内存时,将出OutOfMemoryError。此类错误发生时,JVM将终止线程。
这些错误是不可查的,非代码性错误。因此,当此类错误发生时,应用不应该去处理此类错误。
2、Exception(异常):
程序本身可以捕获并且可以处理的异常。Exception这种异常又分为两类:运行时异常和编译异常。
1、运行时异常(不受检异常):RuntimeException类极其子类表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
2、编译异常(受检异常):Exception中除RuntimeException极其子类之外的异常。如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,否则编译不通过。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。
从另一种角度又可以将java的所有异常分为可查异常(checked exception)和不可查异常(unchecked exception)。
1、可查异常:编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除RuntimeException及其子类外,其他的Exception异常都属于可查异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。
2、不可查异常:编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。
2、异常处理流程:
在java应用中,异常的处理机制分为抛出异常和捕获异常.
抛出异常:当一个方法出现错误而引发异常时,该方法会将该异常类型以及异常出现时的程序状态信息封装为异常对象,并交给本应用。运行时,该应用将寻找处理异常的代码并执行。任何代码都可以通过throw关键词抛出异常,比如java源代码抛出异常、自己编写的代码抛出异常等。
捕获异常:一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常。所谓合适类型的异常处理器指的是异常对象类型和异常处理器类型一致。
对于不同的异常,java采用不同的异常处理方式:
1、运行异常将由系统自动抛出,应用本身可以选择处理或者忽略该异常。
2、对于方法中产生的Error,该异常一旦发生JVM将自行处理该异常,因此java允许应用不抛出此类异常。
3、对于所有的可查异常,必须进行捕获或者抛出该方法之外交给上层处理。也就是当一个方法存在异常时,要么使用try-catch捕获,要么使用该方法使用throws将该异常抛调用该方法的上层调用者。
3、异常的捕获
1、try-catch语句
try { //可能产生的异常的代码区,也成为监控区 }catch (ExceptionType1 e) { //捕获并处理try抛出异常类型为ExceptionType1的异常 }catch(ExceptionType2 e) { //捕获并处理try抛出异常类型为ExceptionType2的异常 }
监控区一旦发生异常,则会根据当前运行时的信息创建异常对象,并将该异常对象抛出监控区,同时系统根据该异常对象依次匹配catch子句,若匹配成功(抛出的异常对象的类型和catch子句的异常类的类型或者是该异常类的子类的类型一致),则运行其中catch代码块中的异常处理代码,一旦处理结束,那就意味着整个try-catch结束。含有多个catch子句,一旦其中一个catch子句与抛出的异常对象类型一致时,其他catch子句将不再有匹配异常对象的机会。
2、try-catch-finally
try { //可能产生的异常的代码区 }catch (ExceptionType1 e) { //捕获并处理try抛出异常类型为ExceptionType1的异常 }catch (ExceptionType2 e){ //捕获并处理try抛出异常类型为ExceptionType2的异常 }finally{ //无论是出现异常,finally块中的代码都将被执行 }
3、try-catch-finally代码块的执行顺序:
A) try没有捕获异常时,try代码块中的语句依次被执行,跳过catch。如果存在finally则执行finally代码块,否则执行后续代码。
B)try捕获到异常时,如果没有与之匹配的catch子句,则该异常交给JVM处理。如果存在finally,则其中的代码仍然被执行,但是finally之后的代码不会被执行。
C)try捕获到异常时,如果存在与之匹配的catch,则跳到该catch代码块执行处理。如果存在finally则执行finally代码块,执行完finally代码块之后继续执行后续代码;否则直接执行后续代码。另外注意,try代码块出现异常之后的代码不会被执行。(见下图:)
4、总结
try代码块:用于捕获异常。其后可以接零个或者多个catch块。如果没有catch块,后必须跟finally块,来完成资源释放等操作,另外建议不要在finally中使用return,不用尝试通过catch来控制代码流程。
catch代码块:用于捕获异常,并在其中处理异常。
finally代码块:无论是否捕获异常,finally代码总会被执行。如果try代码块或者catch代码块中有return语句时,finally代码块将在方法返回前被执行。注意以下几种情况,finally代码块不会被执行:
1、 在前边的代码中使用System.exit()退出应用。
2、 程序所在的线程死亡或者cpu关闭
3、 如果在finally代码块中的操作又产生异常,则该finally代码块不能完全执行结束,同时该异常会覆盖前边抛出的异常。
4、异常的抛出
1、throws抛出异常
如果一个方法可能抛出异常,但是没有能力处理该异常或者需要通过该异常向上层汇报处理结果,可以在方法声明时使用throws来抛出异常。这就相当于计算机硬件发生损坏,但是计算机本身无法处理,就将该异常交给维修人员来处理。
publicmethodName throws Exception1,Exception2….(params){}
其中Exception1,Exception2…为异常列表一旦该方法中某行代码抛出异常,则该异常将由调用该方法的上层方法处理。如果上层方法无法处理,可以继续将该异常向上层抛。
2、throw抛出异常
在方法内,用throw来抛出一个Throwable类型的异常。一旦遇到到throw语句,后面的代码将不被执行。然后,便是进行异常处理——包含该异常的try-catch最终处理,也可以向上层抛出。
但是注意我们只能抛出Throwable类和其子类的对象的异常类。
比如我们可以抛出:
throw new Exception();
有时候我们也需要在catch中抛出异常,这也是允许的,比如说:
Try{ //可能会发生异常的代码 }catch(Exceptione){ throw newException(e); }
5、异常关系链
在实际开发过程中经常在捕获一个异常之后抛出另外一个异常,并且我们希望在新的异常对象中保存原始异常对象的信息,实际上就是异常传递,即把底层的异常对象传给上层,一级一级,逐层抛出。当程序捕获了一个底层的异常,而在catch处理异常的时候选择将该异常抛给上层…这样异常的原因就会逐层传递,形成一个由低到高的异常链。但是异常链在实际应用中一般不建议使用,同时异常链每次都需要就将原始的异常对象封装为新的异常对象,消耗大量资源。现在(jdk 1.4之后)所有的Throwable的子类构造中都可以接受一个cause对象,这个cause也就是原始的异常对象。
下面是一个不错的例子:
/* *高层异常 */ classHighLevelExceptionextends Exception{ public HighLevelException(Throwable cause) { super(cause); } } /* *中层异常 */ classMiddleLevelExceptionextends Exception{ public MiddleLevelException(Throwable cause) { super(cause); } } /* *底层异常 */ classLowLevelExceptionextends Exception{ } publicclass TestException { publicvoid highLevelAccess()throws HighLevelException{ try { middleLevelAccess(); }catch (Exception e) { thrownew HighLevelException(e); } }
publicvoid middleLevelAccess()throws MiddleLevelException{ try { lowLevelAccess(); }catch (Exception e) { thrownew MiddleLevelException(e); } } publicvoid lowLevelAccess()throws LowLevelException { thrownew LowLevelException(); } publicstaticvoid main(String[] args) { /* * lowlevelAccess()将异常对象抛给middleLevelAccess(),而 * middleLevelAccess()又将异常对象抛给highLevelAccess(), *也就是底层的异常对象一层层传递给高层。最终在在高层可以获得底层的异常对象。 */ try { new TestException().highLevelAccess(); }catch (HighLevelException e) { e.printStackTrace(); System.out.println(e.getCause()); } } }
6、异常的转义
异常转义就是将一种类型的异常转成另一种类型的异常,然后再抛出异常。之所以要进行转译,是为了更准确的描述异常。就我个人而言,我更喜欢称之为异常类型转换。在实际应用中,为了构建自己的日志系统,经常需要把系统的一些异常信息描述成我们想要的异常信息,就可以使用异常转译。异常转译针对所有Throwable类的子类而言,其子类型都可以相互转换。
通常而言,更为合理的转换方式是:
1、 Error——>Exception
2、 Error——>RuntimeException
3、 Exception——>RuntimeException,
在下面的代码中,我们自定义了MyException异常类,然后我们将IOException类型的异常转为MyException类型的异常,最后抛出。
class MyExceptionextends Exception { public MyException(String msg, Throwable e) { super(msg, e); } } publicclass Demo { publicstaticvoid main(String[] args)throws MyException { Filefile =new File("H:/test.txt"); if (file.exists()) try { file.createNewFile(); }catch (IOException e) { thrownew MyException("文件创建失败!", e); } } }
7、Throwable类中常用的方法
像catch(Exception e)中的Exception就是异常的变量类型,e则是形参。通常在进行异常输出时有如下几个方法可用:
e.getCause():返回抛出异常的原因。
e.getMessage():返回异常信息。
e.printStackTrace():发生异常时,跟踪堆栈信息并输出。
8、 常见异常总结
java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。
java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。