- 浏览: 43389 次
- 性别:
- 来自: 深圳
最新评论
-
mao_siyu:
这个和 数据量的多少有关系 就像是操作String 使用 c ...
Java解惑之String的"+"一定差于StringBuilder的append吗? -
xiaoxin123go:
我是初学者,虽然没有完全看懂,但是还是学到好多东西。
Java解惑之try catch finally -
Javcoder:
kaobian 写道恕我才疏学 ...
Java解惑之String的"+"一定差于StringBuilder的append吗? -
kaobian:
恕我才疏学浅,我认为用stringbuffer是为了解决内存占 ...
Java解惑之String的"+"一定差于StringBuilder的append吗? -
Javcoder:
回去又看了看那个贴,脑子里又有很多疑问,乱了乱了,各种现象先记 ...
Java解惑之try catch finally
写给自己:
技术关注过于分散往往导致不能专注,长时间的浮躁、纠结最终的结果只是太多珍贵东西浪费,程序员拥有好奇心、求知欲本是件好事,但学会驾驭这些东西才是真正的成熟,坚持并抵住诱惑、潜心而无视喧闹,这是现在自己要做的。
转入正文:
此文起因是由于论坛中出现的这两个讨论贴:
http://www.iteye.com/topic/1112358
http://www.iteye.com/topic/1112387
至于这个问题是否值得深究我们不做讨论,人跟人观点不一样,我就觉得很有意思,所以可以试着分析一下。
不过要提前说明一下,可能有的地方我的理解并不正确或者措辞并不恰当,还希望高手指正。
首先,还是先看下问题,代码如下:
private static void foo() { try { System.out.println("try"); foo(); } catch (Throwable e) { System.out.println("catch"); foo(); } finally { System.out.println("finally"); foo(); } } public static void main(String[] args) { foo(); }
这个会输出什么呢?
要理解这个问题,我们先讲一些其他的东西
1) Java Stacks:
所谓Java栈,描述的是一种Java中方法执行的内存模型,Java栈为线程私有,线程中每一次的方法调用(或执行),JVM都会为该方法分配栈内存,即:栈帧(Stack Frame),分配的栈帧用于存放该方法的局部变量表、操作栈(JVM执行的所有指令都是围绕它来完成的)、方法编译后的字节码指令信息和异常处理信息等,JVM指定了一个线程可以请求的栈深度的最大值,如果线程请求的栈深度超过这个最大值,JVM将会抛出StackOverflowError,注意,此处抛出的时Error而不是Exception。
下面我们来看一张图(引自Inside Java Virtual Machine)
由上图可知:
在一个JVM实例中(即我们运行的一个Java程序)可以同时运行多个线程,而每个线程都拥有自己的Java栈,此栈为线程私有,随着线程内方法的不断调用,线程内的栈深度不断增加,直到溢出。而当一个方法执行完毕(return或throw),该方法所对应的线程内的栈帧被JVM回收,线程内的栈深度会相应的变小,直到线程的终结。
2) Java的异常体系:
在Java的异常体系中,java.lang.Throwable是所有异常的超类,继承于Object,直接子类为Error和Exception,其中Error和RuntimeException(Exception的子类)为unchecked,即:无需用户捕获,除RuntimeException以外的其他Exception都为checked,即:用户必须捕获,否则编译无法通过。
因为Throwable处于Java异常体系的最顶层,所以Java抛出的任何Error和Exception都会被其捕获,包括StackOverflowError。
3) Finally到底是怎么回事?
Finally通常会配合try、catch使用,在每一处的try或catch将要退出该方法之前,JVM都会保证先去调用finally的代码,这里所说的退出不单单是指return语句,try或catch中异常的抛出也会导致相应方法的退出(当然,前提是不被catch捕获以及不被finally跳转)。在执行finally代码时,如果finally代码本身没有退出的语句(return或抛出异常),finally执行完毕后还会返回try或catch,由try或catch执行退出指令。
语言总是缺乏表现力,看代码吧。
public class TCF { static int f1() { try { return 1; } finally { System.out.print("f1"); } } static int f2() { try { throw new Exception("try error"); } catch (Exception e) { return 2; } finally { System.out.print("f2"); } } static int f3() { try { throw new RuntimeException("try error"); } catch (ArithmeticException e) { return 3; } finally { System.out.print("f3"); } } static int f4() { try { throw new Exception("try error"); } catch (Exception e) { throw new RuntimeException("catch error"); } finally { System.out.print("f4"); } } static int f5() { try { throw new Exception("try error"); } catch (Exception e) { throw new RuntimeException("catch error"); } finally { System.out.print("f5"); return 5; } } static int f6() { try { throw new Exception("try error"); } catch (Exception e) { throw new RuntimeException("catch error"); } finally { System.out.print("f6"); throw new RuntimeException("finally error"); } } public static void main(String[] args) { System.out.println(" : " + f1()); try { System.out.println(" : " + f2()); } catch (Exception e) { System.out.println(" : " + e.getMessage()); } try { System.out.println(" : " + f3()); } catch (Exception e) { System.out.println(" : " + e.getMessage()); } try { System.out.println(" : " + f4()); } catch (Exception e) { System.out.println(" : " + e.getMessage()); } try { System.out.println(" : " + f5()); } catch (Exception e) { System.out.println(" : " + e.getMessage()); } try { System.out.println(" : " + f6()); } catch (Exception e) { System.out.println(" : " + e.getMessage()); } } }
输出如下:
解释如下:
声明:我们把每一个可以导致方法退出的点称为结束点。
f1方法: try中return 1代表着try方法块的结束点,jvm会在该结束点执行之前,执行finally,finally代码块本身没有结束点,所以执行完finally后会返回try方法块,然后执行讨try中的return 1,所以结果输出如上。
f2方法:try中throw代表着try方法块的结束点,但是由于有catch的存在,并且catch可以捕获try中抛出的异常,所以catch在某种意义上延续了try的生命周期,try catch此时组成了一个新的整体,try中的throw不再代表一个结束点,而catch中return 2此时代表try catch整体的结束点,这时没有任何语句可以延续try catch的生命周期,JVM知道try catch产生了一个结束点,将要结束方法的执行,所以JVM在这个结束点执行之前立即执行finally,因为finally没有结束点,所以finally执行完毕返回catch,然后执行该catch中的return 2,所以输出结果如上。
f3方法:f3和f2的区别在于f3的catch是捕获ArithmeticException,而我们在try中抛出的是RuntimeException,所以catch没能捕获该异常,也就无法延续try的生命周期,所以try的throw形成一个结束点,JVM获知try将要结束该方法的执行,所以马上调用finally,因为finally内部没有结束点,所以会返回try,然后try抛出自己的异常,输出结果如上。
f4方法:f4和f2本质相同,只不过f2中catch是以return 2作为自己的结束点,而f4中catch是以抛出异常作为自己的结束点,输出如上。
f5方法:f5和f4大部分相同,catch延续try的生命周期,try catch组成一个整体,而这个整体的结束点由catch抛出异常产生,区别就在于下面的部分,JVM知道try catch整体将要结束该方法的执行,所以马上调用finally,而在f5的finally内部有自己的结束点,即:return 5,这样finally自己就结束了整个方法的执行,而不会返回catch,由catch抛出异常,结束该方法的执行,所以会有如上的输出。
f6方法:f6和f5大致相同,只不过在f6的finally中是以抛出异常作为自己的结束点,进而结束方法的执行,输出结果如上。
至此,对于try catch finally的使用都应该大致明白了,但是JVM为什么会这么做呢?它内部究竟是怎么实现的呢?
让我们从字节码的角度来分析一下JVM的try catch finally运行机制。
声明:下面描述的并不是真正的Java字节码,我们只是为了表述方便而模拟出来的。
任何Java类中的方法最终都会编译成字节码,由JVM解释执行,一个Java方法最终形成的就是一串字节码流,下面模拟我们的第一个字节码流:
如果把这段字节码给JVM,JVM就会顺序执行1、2、3、4指令,很简单吧,下面看另一个:
JVM遇到这段指令,会先执行1、2、3,到第4条指令时发现是一个goto语句,所以就跳过5,继而执行6、7,依旧是很简单,然后下一个:
JVM执行1、2、3,然后跳到第7行,执行4、5、6,然后又跳回第5行,执行return 1,看看是否似曾相识,对,它就是我们f1方法的原型!
当我们用javac把f1编译后,生成class文件内部的字节码原理上就和上面的一样,好,既然我们可以模拟f1了,那让我们再来模拟一下f2:
JVM执行1、2、3后看到throw语句,就抛出了一个异常,名字为exception,然后,JVM想应该先去执行finally了,执行完finally后,再把那个异常向上抛,但它又一看,原来还有catch部分,它又看看catch内部,居然有它抛出的那个异常(exception),所以JVM就放弃执行finally部分,转而执行catch的相应部分,即4、5、6,然后它遇到goto(goto是由编译生成,因为编译时它看到一个return 2,它知道这是一个结束点,而Java代码中又有finally语句,所以编译器就会在这个return语句之前生成一条goto语句),所以跳到13,执行7,8,9,最后再跳到11行,执行return 2,这样,我们的f2方法就结束了。
我们再模拟一个f4方法的字节码:
在分析上图之前,我先提出一个问题,当try语句块抛出异常,而我们没有写catch语句块或写的catch语句块中不能捕获try中抛出的异常时,JVM还是能帮我们保证finally的执行,它的内部究竟是怎样实现的呢?好,带着这个疑问,我们来看下面的分析:
在try catch finally语法结构中,try是必须的,而catch和finally中我们至少要选一个,由于这样的语法规则,所以我们可以不写catch,而又由于异常有unchecked的类型(或其他原因),所以很有可能即使我们写了catch,try中抛出的异常在我们的catch中还是不能捕获,综上两种情况我们可以得知,不管怎样,只要有try代码块的地方,就有可能存在我们不能捕获或者说无需捕获的异常。而finally的定义又要求,不管try或catch中发生了什么,finally部分必须要执行。可如果JVM不能捕获上面我们描述的那类异常,它就无法得知一个结束点的产生,也就无法在这种结束点产生的情况下调用finally。Java为了使这两种情况可以同时成立,在遇到有try的代码块地方,Java编译器不管我们有没有声明catch,都会为我们生成一个system catch(命名也许并不恰当),而这个catch可以捕获任何异常,这样一来,即使是上面我们讨论的那种异常产生,JVM也能捕获并得知这可能是一个结束点,进而决定finally是否去执行。分析f4,JVM先执行1、2、3,然后抛出一个异常,我们自定义的catch捕获该异常后,执行4、5、6然后又抛出RuntimeException,由于自定义catch中无法再捕获这个异常,所以由system catch来捕获,system catch只做一件事,调用finally,然后rethrow捕获的异常。
最后我们看下f5:
由于finally内部有自己return了(而不是f4中的goto 19),所以finally中的return 5就代表了整个该方法的退出。
最后,我们再上最后一张截图吧:
这个截图和上面那个截图没有什么不一样,只是去掉了[try] [catch] [finally]等标识符,之所以这样做是因为我想展示的是一个更加贴近真实字节码的模拟。
为什么这样就更加贴近真实了呢?
因为JVM是呆板的,它只知道执行,而没有智能。
对于JVM来说,它并不知道哪处是try,哪处是catch,哪处是finally,甚至对于它来说,根本就没有try catch finally的概念,它知道的只有你给我什么指令,我就执行什么指令,没有语法,没有辨别,它内部没有这样的规定说,啊,12到15行是finally语句块,我得注意点,一旦我遇到一个结束点,我先要跳到finally,执行完这个finally后再跳回这个结束点,然后执行这个结束点,JVM内部并没有这样智能的处理,其实它也不需要有这样智能得处理。Java规范中是要求,只要遇到有finally得地方,不管发生什么情况,finally都要执行,但Java中的这个要求并不是直接对JVM提出的,JVM只是执行指令的机器,而把含有Java语法规范的Java源码翻译成字节码指令的是Javac,对,就是Javac,是Javac把这样的Java语法规范翻译成字节码指令流,而在这些字节码指令流中,通过添加一些判断、跳转、返回等指令,使得当JVM在执行这些指令的时候,它的外部表现就是符合Java语法规范的。
你明白我在说什么吗?
我是在说,任何方法编译后的结果只是一串字节码指令流,各个指令间都是等价的,虽然我们在我们的方法中添加上了try catch finally,但这只是Java语法,编译后的字节码是没有这些东西的,编译的过程是按照Java语法规范生成一系列的包含判断、跳转、返回等指令的指令流,以使JVM在执行这些指令流时并不总是顺序执行,你自己想想,Java语法规范要求的finally特性本质上不就是跳转吗?,finally语法规范用通俗的语句来说就是,在一个含有finally的方法的各个结束点执行之前先跳转到finally,执行完finally后再跳回来,执行剩下的部分,就这么简单。所以,Javac在遇到有finally的方法时,就找出各个方法的结束点,并在各个结束点指令之前添加一条跳转指令,跳转到finally,执行完finally之后,再跳转回来,哇,原来就是些如此简单的东西啊。
此处有一点需要注意的就是当跳转到finally后,如果finally内部有结束点,finally就不会再跳转回去,JVM直接执行了finally内部的结束点(执行其它地方的结束点会先跳转到finally,但执行finally内部的结束点并不会跳转到其它地方,因为这个结束点已经是在finally内部了,无需跳转,所以JVM直接执行了这个结束点,整个方法执行结束),这样finally自己就结束了方法的执行。
最后再说明一点:在一个含有try catch finally的方法中,try语句块内部,catch语句块内部和finally语句块内部的所有语句都有与之对应的字节码指令,所以Javac在编译这些部分的时候,直接编译。而至于try catch finally这三个关键字,它们并没有与之对应的字节码指令,它们只是语法上的定义,Javac在遇到这三个关键字时,会通过其它指令(例如:跳转、返回指令)的组合来实现这种语法要求。
总结一下,try catch finally有两个作用:
1: 把一个方法的字节码指令流分成三个部分,并标识出,哪个部分是try,哪个部分是catch,哪个部分是finally。(各个部分内部也可以存在的跳转,但这种跳转是语句层面的跳转(例如:if),并且这种跳转只能在自己内部发生,即:只能跳到自己内部的其它语句,而不能跳到其它部分的其它语句)
2:指明了这三个部分的执行顺序,例如,先执行try,再执行catch,再执行finally,再执行catch。(这种执行顺序也可以认为是一种跳转,而这种跳转是语法层面的跳转,只能在try catch finally这三个部分之间发生,即:一旦发生跳转就会跳转到其它部分的其它语句,而不是跳转到自己内部的其它语句)
说了这么多,其实我们要记住的只有一点,那就是:要想掌握finally,只需要知道在一个方法中,哪些地方是结束点,即:哪些地方会结束该方法的执行,JVM在这个结束点执行之前,会先去执行finally。
还记得当初那个引出这篇文章的小程序吗?估计都忘了,再回忆一下吧,有一段代码如下:
private static void foo() { try { System.out.println("try"); foo(); } catch (Throwable e) { System.out.println("catch"); foo(); } finally { System.out.println("finally"); foo(); } } public static void main(String[] args) { foo(); }
它会输出什么?
在说明这个问题之前,我首先不得不说一个现象,那就是在不同的机子上运行上面的代码会有不同的输出结果,看看我遇到的三种输出:
1:在公司电脑中,直接执行上面的代码,代码及输出如下:
代码:
public class JvmMain { private static void foo() { try { System.out.println("try"); foo(); } catch (Throwable e) { System.out.println("catch"); foo(); } finally { System.out.println("finally"); foo(); } } public static void main(String[] args) { foo(); } }
输出:
2:在家里的电脑中,直接执行上面的代码,代码及输出如下:
代码和上面的一样,略。
输出:
3:在家里的电脑中,在原来的基础上添加一个方法,代码及输出如下:
代码:
class JvmMain { public static void foo() { try { System.out.println("try"); foo(); } catch (Throwable e) { System.out.println("catch"); foo(); } finally { System.out.println("finally"); foo(); } } public static void fooAgain() throws Exception { throw new Exception("fooAgain"); } public static void main(String[] args) { foo(); } }
输出:
纠结了很久,但依旧不知道是怎么回事,可能是因为JDK的版本或发行商不同吧,不知道,期盼高人分析啊。
按照我的理解,输出结果应该是第一种情况,下面我们就基于第一种情况进行分析:
由于程序是层层递归调用,所以栈的深度会不断增加,直到栈溢出。现在假设我们的栈深度最多能有10层(就是说最多可以存放10个栈帧)
当main中调用foo,foo再调foo,层层递归直到填满第10层。此时,栈及方法执行状态为:由于递归调用,10层栈帧全部填满,此时第10层栈帧对应我们最后调用的那个方法,即:foo。而此时,第10层栈帧对应的foo方法的执行状态为:即将在try中再次调用foo方法,并且希望jvm为此方法分配栈帧,即第11层栈帧,用来存放方法的各种信息,但是,此时的问题就出现了,由于栈内存最多只能分配10层栈帧,所以try中的再次调用foo方法将导致StackOverflowError抛出,而根据我们上面所述,因为第10层栈帧对应的foo方法中存在catch,捕获的是Throwable,所以第10层栈帧对应的foo方法的try中抛出的异常并不代表一个结束点,catch为其延续生命周期,jvm进而执行第10层栈帧对应的foo方法的catch,所以会输出“catch”,然后catch再调用foo,并希望jvm为foo分配栈内存,即第11层栈帧,还是因为栈内存够,catch方法也抛出StackOverflowError,这个Error又被System Catch捕获,System Catch调用第10层栈帧对应的foo方法的finally方法,输出finally,然后第10层栈帧对应的foo方法的finally中再调用foo方法,并希望jvm为其分配内存,内存不够,还是抛出StackOverflowError,此时,finally再次抛出异常,由于该异常成为finally的结束点,所以finally不会再返回system catch,抛出system catch 捕获的catch语句块抛出的异常,jvm执行finally的结束点,退出第10层栈帧对应的foo方法,并且把第10层栈帧内存收回,返回到第9层栈帧对应的foo方法的try语句块中(因为是在此调用的第10层栈帧对应的foo方法),此时第9层栈帧对应的foo方法中的try语句块接到第10层栈帧对应的foo方法返回的异常,try语句块无法处理所以继续抛出异常,由于第9层栈帧对应的foo方法中的catch可以捕获该异常,所以进而执行第9层栈帧对应的foo方法中的catch,输出catch字符串,然后第9层栈帧对应的foo方法中的catch代码块再次调用foo,希望jvm为其分配栈内存,jvm检查栈内存,发现第10层栈帧可以用,所以jvm就为其分配第10层栈帧,分配完成之后,jvm开始执行第10层栈帧对应foo方法的第一条语句,即:输出try字符串,然后jvm开始执行第10层栈帧对应foo方法的第二条语句,即再次调用foo方法,并希望jvm为其分配栈内存,jvm检查之后发现,现在10层栈帧都已经用完,无法再分配了,所以抛出StackOverflowError,之后的jvm行为就和刚刚描述的第10层栈帧对应foo方法是一样的了,最终结果是finally中由于调用foo而jvm无法为其分配第11层栈帧,所以finally抛出异常,返回到第9层栈帧对应的foo方法中的catch中,第9层栈帧对应的foo方法中的catch代码块继续抛出该异常,让其他部分处理,第9层栈帧对应的foo方法的system catch捕获该异常,然后调用第9层栈帧对应的foo方法中的finally语句块,finally中的第一条语句输出finally字符串,第二条语句又调用foo方法,jvm又为该foo方法分配第10层栈帧,后续的执行和第9层栈帧对应的foo方法中的catch中调用foo过程是一样的,结果也是返回StackOverflowError到第9层栈帧对应的foo方法中的finally代码块中,然后,第9层栈帧对应的foo方法中的finally代码块继续向上抛出该异常,并退出第9层栈帧对应的foo方法,回收第9层栈帧占用的内存,第8层栈帧对应的foo方法的try代码块接到该异常并继续抛出,然后。。。
后续的部分不再分析,因为我想如果你还没有被绕晕,你肯定是已经理解了,那后续的部分自己已经可以推导出来。
直接看我的推导结果吧,我只分析了栈最上面的三层:
怎样看这个图呢?等号划分三个部分,从上到下依次读取三个部分的字符串输出,如果一个部分中有多行,则把上面的行压倒最下面的行的空白处,例如第二部分,将10压入到9的空白处,形成输出为:catch try catch finally finally try catch finally,把三个部分形成的一个大的字符串和程序的输出结果进行比较,结果完全一样(当然,要从开始抛出异常的地方进行比较)。
按照这种分析,这段递归程序最终的最终会抛出异常,因为最底层的main方法无法处理上一层foo的finally抛出的StackOverflowError,但我在公司跑了一下午都没有出现这种结果,哎,很受打击,但后来我想了想,一下午的时间真的够吗?
假设我们的栈的最大深度为2001,那让我们粗略的算算有多少次的栈帧分配和释放的过程?至少是3的2000次方以上吧,这个数量需要多久??而你再看看你自己栈最大深度,远远不止2000吧。
到此为止,所以的分析完毕,但还是有些疑问不能解释:
疑问1:java栈深度是否会根据栈内存使用情况动态变化?
因为在一长串的try输出中,我无意间发现了一个catch,这是我公司电脑的输出,而家里的电脑就没有这种输出。
疑问2:是否会因为jdk版本、发行商或是参数设置的问题导致这段程序的输出结果不同?(上面说的三种输出结果中的1和2)
疑问3:为什么我加了一个没有用到的方法(加的方法必须要抛异常才可以)会改变原来的输出?(上面说的三种输出结果中的2和3)
疑问4:为什么上面的输出有些不换行?
评论
1) catch内容由Throwable换成StackOverflowError,输出也如我上面说的第一种情况,即:有catch字符串输出
public static void foo() { try { System.out.println("try"); foo(); } catch (StackOverflowError e) { System.out.println("catch"); foo(); } finally { System.out.println("finally"); foo(); } }
第9页得讨论:http://www.iteye.com/topic/1112387?page=9
slangmgh说如果catch的是Throwable,Throwable类不会加载,所以异常匹配不上,似乎有些道理,但我上面说的第三种情况又怎么解释呢?(加一个方法,用来抛出异常,但该方法并没有使用)
还有,下面的代码catch为什么又能捕获?
public static void foo() { try { System.out.println("try"); foo(); } catch (Throwable e) { System.out.println("catch"); // foo(); } // finally { // System.out.println("finally"); // foo(); // } }
输出:
...
try
try
try
try
trycatch
2) StackOverflowError到底能不能捕获?我测试明明可以,但为什么很多人说不能?他们说的貌似很有底气啊。
相关推荐
java解惑java解惑java解惑java解惑java解惑java解惑
Java解惑Java解惑Java解惑Java解惑Java解惑Java解惑Java解惑Java解惑Java解惑Java解惑Java解惑Java解惑
Java解惑.pdf Java解惑.pdf Java解惑.pdf Java解惑.pdf
Java解惑中文版 Java解惑 java健壮程序
JAVA解惑.pdf JAVA解惑.pdf JAVA解惑.pdf
与java相关的的学习,适合初学者,可以看看
Java PUZZLE Java 解惑 Java PUZZLE Java 解惑 Java PUZZLE Java 解惑Java PUZZLE Java 解惑 Java PUZZLE Java 解惑 Java PUZZLE Java 解惑
《Java解惑》 布洛克 著;陈昊鹏 译 扫描清晰带目录,仅供参阅,请支持正版
JAVA解惑,你面包括一些java经典的问题。
《Java解惑》《Java解惑》《Java解惑》《Java解惑》《Java解惑》《Java解惑》
"java解惑" PDF版本
讲述如何在程序中避免程序缺陷和程序陷阱的,解惑的过程中,介绍了一些Java编程语言中许多不易被掌握的知识点,其阅读价值非常高,适合具有Java知识的学习者和有编程经验的Java程序员阅读。
Java解惑(中文).pdf 给大家介绍java中容易迷惑用错的实例
Java四大名著之一:4,JAVA解惑 高清PDF 下载
Java解惑,是一本以大量java实例,讲述如何在程序中避免程序缺陷和程序陷阱的,解惑的过程中,介绍了一些Java编程语言中许多不易被掌握的知识点,其阅读价值非常高,适合具有Java知识的学习者和有编程经验的Java...
该书特写了95个有关Java或其类库的陷阱和缺陷的谜题,其中大多数谜题都采用了短程序的方式,这些程序的行为与其看似的大相径庭。在每个谜题之后都给出了详细的解惑方案,这些解惑方案超越了对程序行为的简单解释,向...
。。。。。 Java解惑(中文) 是一本对 Java一些问题的解答 。。。。。。。。。。
java 解惑 java 解惑 java 解惑 java 解惑 java 解惑 java 解惑 java 解惑 java 解惑 java 解惑
java 解惑,pdf绝对清析版本,有目录。