前言:
现在朋友们对“jvm异常处理”大约比较讲究,朋友们都需要知道一些“jvm异常处理”的相关文章。那么小编同时在网上汇集了一些关于“jvm异常处理””的相关内容,希望咱们能喜欢,我们快快来了解一下吧!各种编程语言都有自己的异常捕获、处理方式。在程序指令执行过程中,可能会发生可预知或不可预知的各种异常。通过异常捕获和处理可以记录相关异常日志,执行一些补救策略,让局部的错误不影响服务整体可用性。
那么Java的异常处理代码是如何被编译的?JVM又是如何捕获异常、执行异常处理逻辑的?
抛出异常:athrow指令
在Java编程语言中,使用throw关键字来抛出异常
void cantBeZero(int i) throws TestExc { if (i == 0) { throw new TestExc(); }}
编译后:
0 iload_11 ifne 124 new #2 <com/supercoder/jvm/TestExc>7 dup8 invokespecial #3 <com/supercoder/jvm/TestExc.<init>> 11 athrow12 return
athrow指令从操作数栈栈顶拿到一个TestExc实例的引用抛了出去。TestExc实例创建的过程不在这里讲了,在实例创建和方法调用的相关章节有详细讲解,可以找一找历史文章复习一下。
接着往下看,如何捕获、处理异常?
抓住异常:try-catch
在Java编程语言中,使用try将代码括起来,使用catch捕获指定的异常类型。try-catch支持有一个或多个catch子句,并且支持嵌套。
单个catch子句
void catchOne() { try { tryItOut(); } catch (TestExc e) { handleExc(e); }}
编译后:
Method void catchOne()0 aload_01 invokevirtual #64 return5 astore_16 aload_07 aload_18 invokevirtual #511 returnException table:From To Target Type0 4 5 Class TestExc
使用了try-catch之后,try代码块在编译时并没有特殊的改变,和没有try时一样。如果try代码块在执行过程中没有抛出任何异常,那么会和没有try时的表现一样:完成对tryItOut方法的调用,且catchOne方法正常返回。对handleExc方法的调用、以及catch子句中的内容都会作为正常的方法调用被编译。紧跟在try代码块指令后面的就是单个catch子句的指令。
由于catch子句的存在,编译器会生成一个异常表,[From,To)是一个左闭右开的区间,Target指向对应的异常处理器第一个指令位置。catchOne方法的异常表中有一个关联到TestExc实例的条目,catchOne方法的catch子句可以访问这个TestExc实例。在执行索引是0到4区间的指令时,如果有TestExc类型的值被抛出,控制将会转移到索引为5的指令,即catch子句代码块。如果抛出的值不是TestExc类型,catch子句是不能被执行的,这个值会被重新抛给catchOne方法的调用者。
多个catch子句
一个try也可以有多个catch子句。
void catchTwo() { try { tryItOut(); } catch (TestExc1 e) { handleExc(e); } catch (TestExc2 e) { handleExc(e); }}
编译后:
Method void catchTwo()0 aload_01 invokevirtual #54 return5 astore_16 aload_07 aload_18 invokevirtual #711 return 12 astore_113 aload_014 aload_115 invokevirtual #718 returnException table:From To Target Type0 4 5 Class TestExc10 4 12 Class TestExc2
一个try声明的多个catch子句在编译时也很简单,每一个catch子句编译后的指令序列都跟在另一个catch子句对应的指令后面,并且会添加一个条目到异常表中。
在索引0到4区间的try代码块运行期间,如果有一个值被抛出,该值的类型和一个或多个catch子句参数中的实例相匹配时,匹配的第一个catch子句会被选中,控制转移到这个catch子句的代码块。反之,如果抛出的值不能跟任何一个catch子句参数中的实例匹配。JVM会重新把这个值抛出给catchTwo方法的调用者,并且不会执行任何一个catch子句。
嵌套try-catch
嵌套try-catch的编译跟多个catch子句的编译非常相似。
void nestedCatch() { try { try { tryItOut(); } catch (TestExc1 e) { handleExc1(e); } } catch (TestExc2 e) { handleExc2(e); }}
编译后:
Method void nestedCatch()0 aload_01 invokevirtual #84 return5 astore_16 aload_07 aload_18 invokevirtual #711 return 12 astore_113 aload_014 aload_115 invokevirtual #6Exception table:From To Target Type0 4 5 Class TestExc10 12 12 Class TestExc2
try-catch的嵌套只会在异常表中有所体现。每一个try-catch都对应异常表中的一个条目,它们From和To所表示的区间会出现交集。JVM并没有强制异常表条目的嵌套以及排序规则,不过,由于try-catch的构造是结构化的,编译器是可以对异常处理表中这些结构化数据进行排序的(例如可以按照Target从低到高排序)。排序之后,对于方法中抛出的任何异常,第一个与之匹配的异常处理程序就是最内层的匹配catch子句,内层不能匹配时会继续向外层尝试匹配。
兜底的finally
无论try代码块中是否有异常抛出,无论异常是否被catch住,finally代码块一定会被执行。
没有catch的try-finally
生成class文件的版本为50.0(即JDK1.6)及以下的编译器,在编译包含finally的异常处理代码时可能会同时使用两个特殊的指令:jsr(跳转到子程序)、ret(从子程序返回)。finally子句被作为方法的子程序被编译,类似异常处理器。
void tryFinally() { try { tryItOut(); } finally { wrapItUp(); }}
编译后:
Method void tryFinally()0 aload_01 invokevirtual #64 jsr 147 return8 astore_19 jsr 14 12 aload_113 athrow14 astore_215 aload_016 invokevirtual #5 19 ret 2Exception table:From To Target Type0 4 8 any
当没有catch,只有finally时,编译器也会在异常表中生成一个条目,Type是any,任何类型的异常都能够与之相匹配。当调用子程序的jsr指令被执行时,紧跟在jsr后指令的地址会作为returnAddress类型先被压入操作数栈,子程序代码会将这个地址存入局部变量表。子程序代码的尾部会有一个ret指令,该指令从局部变量表获取到返回地址并将控制转移到该地址处的指令。
一个完整的异常处理结构:try-catch-finally
void tryCatchFinally() { try { tryItOut(); } catch (TestExc e) { handleExc(e); } finally { wrapItUp(); }}
编译后:
Method void tryCatchFinally()0 aload_0 //将局部变量表0处的this压入操作数栈(当前方法的调用者引用)1 invokevirtual #4 //调用tryItOut方法4 goto 16 //控制转移到index 16,执行jsr指令7 astore_3 //[0,4)之间出现了异常,抛出的异常值存入局部变量表3的位置8 aload_0 //this压栈9 aload_3 //异常值压栈10 invokevirtual #6 //调用handleExc方法13 goto 16 //控制转移到index 16,执行jsr指令16 jsr 26 //控制转移到index 26,执行finally子程序(会先把19压入操作数栈)19 return //执行finally子程序后,本次方法调用返回20 astore_1 //TestExc类型不能与异常值类型匹配,异常值存入局部变量表1的位置21 jsr 26 //控制转移到index 26,执行finally子程序(会先把24压入操作数栈)24 aload_1 //将局部变量表1处的异常值压入操作数栈25 athrow //将异常重新抛出26 astore_2 //从操作数栈弹出原始执行位置(19或24),存入局部变量表2的位置27 aload_0 //局部变量表0处的this压入操作数栈28 invokevirtual #5 //调用wrapItUp()31 ret 2 //从局部变量表2的位置取出原始指令索引(19或24),控制转移到对应位置Exception table:From To Target Type0 4 7 Class TestExc0 16 20 any
当try代码块正常完成时,会由goto指令将控制转移到索引16的位置,调用jsr指令去执行finally子程序。如果try代码块发生了异常且异常类型是TestExc,根据异常表的Target,控制会转移到索引7的位置,执行对应的异常处理逻辑,之后还会由goto指令将控制转移到索引16的位置,同样由jsr去调用finally子程序。若异常类型不能与catch子句的异常类型匹配,则会与any匹配,对应的异常处理逻辑会先调用finally子程序,然后将异常继续抛出给方法的调用者,
finally何时被执行?
有4种不同的方式,可以让finally子程序被调用。
try子句正常执行完成,此时会在执行下一个指令前,通过jsr指令调用finally子程序。try子句内部的break或者continue子句将控制转移到try子句外时,会先通过jsr调用finally子程序。try子句中调用了return,会先通过jsr调用finally子程序。引发一个异常。
50.0版本之后finally是如何被编译的?
50.0版本之后(从JDK1.7开始),不再使用jsr和ret指令执行finally指令的调用。取而代之的是一种非常简单暴力的方式:将finally子程序的指令序列编译到在每一个需要调用finally子程序的位置,这样做的代价就是字节码文件有所膨胀。
我们以JDK1.8为例,有如下代码:
public int test() { int i = 1; try { i++; return i; } catch (Exception e) { i++; return i; } finally { i = 10; }}
编译后:
0 iconst_11 istore_12 iinc 1 by 15 iload_16 istore_27 bipush 10 //try正常执行,方法return之前执行finally逻辑9 istore_110 iload_211 ireturn12 astore_2 //异常与catch子句匹配,执行异常处理13 iinc 1 by 116 iload_117 istore_318 bipush 10 //catch子句逻辑执行完,在return之前先执行finally逻辑20 istore_121 iload_322 ireturn23 astore 425 bipush 10 //try代码块抛出了异常,catch子句没有匹配上,先执行finally逻辑,然后将异常重新抛出给调用者27 istore_128 aload 430 athrowException table:From To Target Type2 7 12 java/lang/Exception2 7 23 any12 18 23 any23 25 23 any
从编译结果可以看到,没有出现jsr和ret指令,finally的指令序列重复出现在三个地方。
经典面试题
在本文最后一个例子中,test方法的局部变量:i,其值最终是多少?test方法的返回值又是多少?请结合编译结果进行分析,欢迎留言讨论。
标签: #jvm异常处理