Atitit.异常机制的设计原理
缺陷
关键是只要知晓有一个异常表的存在,try 的范围就是体现在异常表行记录的起点和终点。JVM 在 try 住的代码区间内如有异常抛出的话,就会在当前栈桢的异常表中,找到匹配类型的异常记录的入口指令号,然后跳到该指令处执行。异常指令块执行完后,再回来继 续执行后面的代码。JVM 按照每个入口在表中出现的顺序进行检索,如果没有发现匹配的项,JVM 将当前栈帧从栈中弹出,再次抛出同样的异常。当 JVM 弹出当前栈帧时,JVM 马上终止当前方法的执行,并且返回到调用本方法的方法中,但是并非继续正常执行该方法,而是在该方法中抛出同样的异常,这就使得 JVM 在该方法中再次执行同样的搜寻异常表的操作。作者:: 绰号:老哇的爪子 ( 全名::Attilax Akbar Al Rapanui 阿提拉克斯 阿克巴 阿尔 拉帕努伊 ) 汉字名:艾龙, EMAIL:1466519819@qq.com
转载请注明来源: http://blog.csdn.net/attilax
上面那样的内层方法无法处理异常的层层向外抛,层层压栈,这样就形成一个异常栈。异常栈十分有利于我们透析问题之所在,例如 e.printStackTrace(); 或者带参数的 e.printStackTrace(); 方法可将异常栈信息定向输出到他处,还有 log4j 的 log.error(Throwable) 也有此功效。若是在行径的哪层有能力处理该异常则已,否则直至 JVM,直接造成 JVM 崩溃掉。例如当 main() 方法也把异常抛了出去,JVM 此刻也就到了生命的尽头。
解释一下这段字节码的运作过程,如果平时看字节码比较多的同学可以直接略过这段。偏移地址为0的new指令首先会在常量池找到第58项常量,此常量现在为CONSTANT_Class_info型的符号引用,类解析阶段被翻译为java.lang.Exception类的直接引用,接着虚拟机会在Java堆中开辟相应大小的实例空间,并将此空间的引用压入操作栈的栈顶。偏移为3的dup指令就简单的把栈顶的值复制了一份,重新压入栈顶,这时候操作栈中有2份刚刚new出来的exception对象的引用。偏移为4的invokespecial指令将第一个exception对象引用出栈,以它为接收者调用了Excepiton类的实例构造器,这句执行完后栈顶还剩下一份exception对象的引用。写了那么多,说白了这3条字节码就是干了“new Exception()”这句Java代码应该做的事情,和创建任何一个Java对象没有任何区别。这一部分耗费的时间在上一节中分析过,创建一个异常对象只占创建、抛出并接住异常的20%时间。 接着是占用80%时间高潮部分,偏移为7的athrow指令,这个指令运作过程大致是首先检查操作栈顶,这时栈顶必须存在一个reference类型的值,并且是java.lang.Throwable的子类(虚拟机规范中要求如果遇到null则当作NPE异常使用),然后暂时先把这个引用出栈,接着搜索本方法的异常表(异常表是什么等写完这段再说),找一下本方法中是否有能处理这个异常的handler,如果能找到合适的handler就会重新初始化PC寄存器指针指向此异常handler的第一个指令的偏移地址。接着把当前栈帧的操作栈清空,再把刚刚出栈的引用重新入栈。如果在当前方法中很悲剧的找不到handler,那只好把当前方法的栈帧出栈(这个栈是VM栈,不要和前面的操作栈搞混了,栈帧出栈就意味着当前方法退出),这个方法的调用者的栈帧就自然在这条线程VM栈的栈顶了,然后再对这个新的当前方法再做一次刚才做过的异常handler搜索,如果还是找不到,继续把这个栈帧踢掉,这样一直到找,要么找到一个能使用的handler,转到这个handler的第一条指令开始继续执行,要么把VM栈的栈帧抛光了都没有找到期望的handler,这样的话这条线程就只好被迫终止、退出了。
Java代码
1. Exception table:
2. from to target type
3. 0 8 8 Class java/lang/Exception
上面的异常表只有一个handler记录,它指明了从偏移地址0开始(包含0),到偏移地址8结束(不包含8),如果出现了java.lang.Exception类型的异常,那么就把PC寄存器指针转到8开始继续执行。顺便说一下,对于Java语言中的关键字catch和finally,虚拟机中并没有特殊的字节码指令去支持它们,都是通过编译器生成字节码片段以及不同的异常处理器来实现
字节码指令还剩下2句,把它们说完。偏移地址为8的astore_1指令,作用是把栈顶的值放到第一个Slot的局部变量表中,刚才说过如果出现异常后,虚拟机找到了handler,会把那个出栈的异常引用重新入栈。因此这句astore_1实现的目的就是让catch块中的代码能访问到“catch (Exception e)”所定义的那个“e”,又顺便提一句,局部变量表从0开始,第0个Slot放的是方法接收者的引用,也就是使用this关键能访问的那个对象。最后的return指令就不必多讲了,是void方法的返回指令,因为我们的catch块里面没有内容,所以立刻就return了。 到此为止,这几句字节码讲完了,我们总结一下athrow指令中虚拟机可能做的事情(只会做其中一部份啦):
· 检查栈顶异常对象类型(只检查是不是null,是否referance类型,是否Throwable的子类一般在类验证阶段的数据流分析中做,或者索性不做靠编译器保证了,编译时写到Code属性的StackMapTable中,在加载时仅做类型验证)
· 把异常对象的引用出栈
· 搜索异常表,找到匹配的异常handler
· 重置PC寄存器状态
· 清理操作栈
· 把异常对象的引用入栈
· 把异常方法的栈帧逐个出栈(这里的栈是VM栈)
· 残忍地终止掉当前线程。
· ……
第一句提取操作栈中引用的异常对象,第二句检查异常是否为空,虚拟机规范中要求的为null就当NPE异常,就是这句实现的:
注释中说可以使用“common code”是指handle_return中的代码,每条opcode处理完都会转到这段代码。因为异常不一定来自athrow指令,也就是不一定来自于用户程序直接抛出,虚拟机运作期间也会产生异常,如被0除、空指针,严重一点的OOM神马的。所以出现异常后的方法退出动作在通用的handle_return里面根据pending_exception进行处理,代码太多就不贴了。前面几句没有太特别的动作,看来athrow指令的关键实现还是在handle_exception这节,看看它的代码(为了逻辑清晰,我删除了不必要的代码,譬如支持跟踪调试的语句)
看这段代码的关键部分,CALL_VM那句是查找异常表,所执行的InterpreterRuntime::exception_handler_for_exception在同目录下的interpreterRuntime.cpp中,查找的具体过程有点复杂,只看程序的主体脉络,这里的代码就不再牵扯进来了。如果找到,也就是if (continuation_bci >= 0)成立的话,(bci的意思是Bytecode Index,字节码索引),把异常对象重新入栈(SET_STACK_OBJECT(except_oop(), 0)这句),并且重置PC指针为异常handler的起始位置(pc = METHOD->code_base() + continuation_bci这句),然后跳转到run处开始下一轮的循环switch过程。查询异常表没有找到合适的handler,那重新设置上pending_exception,因为前面的时候使用clear_pending_exception()清除掉了。在handle_return中会根据这个标志来决定方法是否出现异常,要不要退出。虚拟机规范中要求的athrow指令的动作这里就写完了,HotSpot VM我们写不出来,看一下还是可以的嘛。
透过JVM看Exception本质 - Java综合 - Java - ITeye论坛.htm
JVM 对Java异常的处理原理 - 旭灿的专栏 - 博客频道 - CSDN.NET.htm