我对Java的异常处理机制的理解
首先从一个问题引出一个角度:跑java程序的机器在如何运行?
Jvm
和物理机器构成了一个运行环境。这个运行环境“吸收”提供的class文件或其他资源,包括java main入口的给定参数等等,以多线程thread形式运行着。线程在另一个线程中诞生出来,某一天魂归西天。在线程诞生之初就属于一个分组threadgroup。
然后描述另外一个角度:所谓运行环境吸收的材料中,比如class文件,其实是我们程序员给的运行规定。就是说我们按java language specification给出了我们对如何运行的命令信息。实际运行和我们的意愿之间到底是怎么相关呢?
运行环境的出现之初就会按我们的意愿试图执行某个类的main.首先运行环境会诞生一个主线程,这个主线程首先要求运行环境已经initializing这个类,没有就得initializing这个类,在做initializing之前又得要求linking,以致loading,不管怎么样,这样的约定导致主线程会运行loading,linking,initializing,然后再运行main方法里我们表达的意思。当然在这个主线程里也会按我们的意思诞生新线程,这个新线程也还是得在诞生之初按我们的意愿试图执行那个runable类的run方法,这当然也得要求initializing。所不同的是有可能这个initializing在某个其他线程里给运行了,那么也就是说运行时环境满足了这个要求,那在当前线程中就不干这个事了。
最后我们来看异常的概念。
显然我们的意愿是不容违背的。呵呵。可是现实归现实,总有那么多不尽人意。当运行时环境参照我们的意愿以各个线程运行时,总可能出现点违背意愿的事,也就是异常。这个情况的出现可能是机器内存条抖了下,可能是我们的意愿就不正常,比如想数组越界访问,或者我们的意愿就是“请此刻马上当作异常发生了”等等。那么这时会怎么运行呢?
这种情况当然是在线程中发生的,发生后运行时大师就会根据当前情况构造一throwable实例,然后寻找本线程要转到哪个点上去继续运行。这到底是哪个点,这个意愿当然还得我们程序员通过java language specification给出命令信息。我们的意愿不一定非要写出来,有一个是约定好的:异常传播跳出前释放同步块的锁。那么如果运行时大师找到了这个点,就会安排本线程继续从这个点运行,找不到,那么运行时大师就会把该线程杀了。杀之前先执行它的threadgroup的uncaughtException方法里我们表达的意愿。注意不管是在那个点还是这个方法,我们都有机会表达对那个throwable实例怎么怎么样的想法。
Ok,
现在基本框架已经有了,再具体的描述下。
刚才说到了每个线程的运行,异常是有很多种情况的。那这种情况算什么时候发生的呢?总起来说就是什么时候触发了执行,什么时候算。比如我们有一句a=1/b.当线程参照这句执行时,发现b=0,那就可以说在这时发生了个异常。再比如我们某StringUtils.contact(s1,s2);当线程参照这句执行时,可能就会要求先做StringUtils的resolving(linking的最后一个可选步骤),但也可能在线程link该句所属的类时就会做,这根据jvm规范得看jvm怎么实现了。但不管怎么说,什么时候做的resolving,而在这个过程中出异常了,就得算这个点。这样就有一个意识:发生点是可以嵌套的。比如a方法a步调用b方法,但b里b步抛异常了,我们可以说b步是个点,但对a来说,a步也是这个点。最原始的那个点怎么算?怎么说都无所谓。但有谓的是下面:
运行时大师就会根据当前情况构造一个throwable实例!其中当前情况就有一个当前线程的执行栈信息。这个当前信息怎么算有所谓。而且,然后寻找线程要转到哪个点上去继续运行。从哪里开始找起也有所谓。这两个所谓都在jvm中规范了。
关键是我们程序员如何表达。
-
1
当前信息就是线程当时的栈信息。注意这个栈信息是我们的程序(包括Lib)的某个方法名的一些信息,而不是细化到某个操作2进制指令。
-
我们的程序中哪句是最终导致异常的,从这一句所在的代码块开始找起。所以主线程一开始时initialize那个main类,如果jvm实现是最早link,那么可能会由此递归initialize很多main类应用的类,如果这个过程中出异常了,我们甚至就没有必要找了,之后怎么处理无所谓;如果initialize后执行main方法过程中某句导致出异常了,那就得从这一句所在的代码块开始找起。Try{
~~~;a;~~~;}catch{}finally{}
。Catch不匹配就要跳出这个try块,但跳出之前也要先让本线程运行完finally。如果catch,finally又抛异常了,那就形成一个新异常,原来的异常的信息不见了,所以要小心。注意“那个最终导致异常的那一句”是这个意思,比如Try{~~~;a();~~~;}catch{}finally{}。a()出异常了,但不是a()句,而是a()方法中出异常的最终那一句,由此递归到程序可见的最终一句。从那个地方找起。
异常多数是同步发生的,就是在线程的某个固定环节发生,但也有异步的,比如内存溢出,谁也不知道会在哪个线程中哪个点发生,对于这样的异步异常,jvm规范中说jvm实现让这种异常实际发生后代码继续运行一有限段,所以给出一个简单实现模型:运行时对这种异常的检测时每次线程做控制转移指令时,比如条件转移啊等等。
但是无论什么异常,对于我们用java表达的意愿,必须满足:异常逻辑发生时之前的java都执行了,之后的都没有执行。至于实际实现中,可能我们看到的异常发生时刻和实际的发生时刻有差别(因为运行时检测不是实时的),也可能我们看到的代码执行和编译器优化后实际的执行顺序不一致,但不管怎么样,我们看到的和表达的必须一致。
最后看异常的分类。我截至目前说的异常都是一个广义的概念,实际上应该对应throwable这个类。Throwable之下有exception和error.exception下有很多,其中有一个是RuntimeException。我们用他们表达对运行的意愿,同时我们也用他们表达我们对提供的接口运行时可能出现的异常的提示。这种表达是java的特色,一般语言不会直接表达这种可能性。假如说我们的某个方法可能抛exception,意思是别人使用这个方法时能处理这个异常,对非runtimeexception一定要么捕捉下,要么继续传播;对runtimeexception,可以不做处理。而假如说我们的某个方法可能抛error,意思则是别人用这个方法可能会面对error,而且不认为别人能处理。从另一个方面,对于我们使用某个方法,如果他声明会抛一个非runtimeexception,我们必须处理,真有error的可能我们都不用,当然也可能记下日志就算了。考虑到这种含义,编译器帮助我们一定要处理非runtimeexception。