大家好,我是伟伟。比如下面这位读者:他在看完我的文章《神了!异常信息突然就没了?》之后有一个疑问。由于是看了我的文章带来的进一步的思考,所以刚好我知道了。虽然这种文章很少有人看,但我还是来填坑的。靠,真是个石锤暖男。如何抛出异常。先从一个简单的代码片段说起:运行结果大家都很熟悉了。光看这几行代码,我们探索不出什么有价值的东西。我们都知道它是如何工作的,它没有任何问题。这就是知道了。所以为什么?所以,它隐藏在代码后面的字节码中。通过javap编译后,上述代码的字节码如下:我们主要关注以下几个部分,同时对字节码指令的含义进行注释:publicstaticvoidmain(java.lang.String[]);code:0:iconst_1//将int类型1压入栈顶1:iconst_0//将int类型0压入栈顶2:idiv//将两个int值相除压顶入栈并将结果压入栈顶3:istore_1//将栈顶的int值保存到第二个局部变量中4:return//从当前方法返回void不要问我怎么做知道字节码的意思,翻桌子就行了,这玩意谁能背得住。通过字节码,似乎没有什么玄机。但是,您首先要记住这一点,我会立即向您展示一个转换:publicclassMainTest{publicstaticvoidmain(String[]args){try{inta=1/0;}赶上(异常e){e。打印堆栈跟踪();}}}用try-catch包装代码以捕获异常。再次用javap编译后,字节码变成了这样:可以明显看出字节码变了,至少变长了。主要关注我框架的部分。对比一下两种情况的字节码:对比之后就很清楚了。添加try-catch后,原来的字节码指令就少了好几行。没有加框的是额外的字节码指令。至于extra的部分,一个叫Exceptiontable的特别明显:.png)exceptiontable,JVM用来处理异常的。至于这里每个参数的含义,我们直接绕过网上的“二手”资料,去官网找文档:https://docs.oracle.com/javas...好像有英文很多,压力很大,不过别怕,我有我,我挑重点跟你说:首先,start_pc和end_pc是一对参数,对应Exception表中的from和to,表示异常的覆盖范围。比如前面的from是0,to是4,exception代表的字节码索引是这个范围:0:iconst_1//将int类型1压入栈顶1:iconst_0//将int类型压入栈顶0tothestackStacktop2:idiv//将栈顶的两个int值相除,并将结果压入栈顶3:istore_1//里面有关于存放int值的细节栈顶进入第二个局部变量,不知道大家注意到没有。范围不包含4,范围区间为[start_pc,end_pc)。至于为什么不包含end_pc,这个有点意思。拿出来说说。end_pc是独占的这一事实是Java虚拟机设计中的一个历史性错误:如果Java虚拟机代码的一个方法恰好是65535字节长并且以1字节长的指令结束,那么该指令就不能被保护通过异常处理程序。编译器编写者可以通过将为任何方法、实例初始化方法或静态初始化程序(任何代码数组的大小)生成的Java虚拟机代码的最大大小限制为65534字节来解决此错误。Notincludedend_pc是JVM设计过程中的一个历史性错误。因为如果JVM中一个方法编译后的代码恰好是65535字节长,并且以1字节长的指令结尾,那么这条指令是无法被异常处理机制保护的。编译器作者可以通过限制任何方法、实例初始化程序或静态初始化程序生成的代码的最大长度来解决此错误。以上是官网上的解释,反正是半懂不懂。没关系,跑个例子:当我的代码只有一个方法,长度为16391行时,编译后的字节码长度为65532。从前面的分析我们知道,一行a=1/0代码会被编译成4行字节码。所以只要我再添加一行代码,就会超过限制。如果我此时编译代码,会发生什么?看图:直接编译失败,告诉你代码太长了。所以你现在知道了一个知识点:方法的长度在字节码级别是有限制的。但是这个限制比较大,一般人写不出这个长度的代码。虽然这个知识点没什么用,但是如果你在工作中真的遇到了一个几万行长度的方法,即使不触发字节码长度限制,我也送你一句话:快跑。接下来,下一个参数handler_pc对应Exception表中的target。其实很好理解,就是指异常处理器启动的指令对应的索引。比如这里的target是7,对应astore_1指令:.png),告诉JVM如果发生异常,请从这里开始处理。最后看catch_type参数,对应Exception表中的type。这是程序捕获的异常。比如我修改程序,捕获三种类型的异常:那么编译后的字节码对应的异常表可以处理的类型就变成了这三种:至于为什么我这里不能写String呢?不要问,问是语法规则。具体的语法规则是什么?就在异常表中:编译器检查该类是Throwable还是Throwable的子类。关于Throwable、Exception、Error、RuntimeException我就不细说了,只是生成一个继承关系图给大家看看:所以,上面的信息总结一下:来自:可能发生异常的起点指令索引下标(included)to:可能发生异常的结束点Instructionindexsubscript(notincluded)target:from和to范围内,异常发生后,开始处理异常的指令索引下标type:异常类信息可以在当前范围内处理知道了异常表之后,问题就可以回答了:异常是怎么抛出的?JVM通过异常表为我们抛出。异常表中有什么?我之前说过,所以我不再重复。如何使用异常表?简要说明:1、如果发生异常,JVM会在当前方法中查找异常表,看是否捕获到异常。2、如果在异常表中发现异常,则调用目标对应的索引下标的指令,继续执行。OK,那么问题又来了。如果没有匹配到异常怎么办?我在官网文档这里找到了答案:https://docs.oracle.com/javas...它的示例代码是这样的:然后下面有一段描述:表示如果抛出的值与catchTwo的任何catch子句的参数如果不匹配,Java虚拟机将重新抛出该值,而不调用catchTwo的任何catch子句中的代码。什么意思?说白了,反正我是处理不了,把异常抛给调用者。这是编程常识,当然大家都知道。但是当常识性的东西用这样规范的描述展现在你面前的时候,还是觉得挺神奇的。当有人问你为什么调用过程是这样的时候,你说这是一个规则。当有人问你规定在哪里时,你可以拿出官网文件扔到脸上,指着说:就是这个。虽然,好像没什么用。对于稍微特殊的情况,我简单介绍下finally的情况:publicclassMainTest{publicstaticvoidmain(String[]args){try{inta=1/0;}赶上(异常e){e。打印堆栈跟踪();}最后{System.out.println("final");}}}javap编译后,异??常表中有3条记录:第一条是我们主动捕获的异常。第二个第三个都是any,这是什么?答案在这里:https://docs.oracle.com/javas...主要看我划清界线的地方:带有finally子句的try语句被编译成有一个特殊的异常处理程序,异常处理程序Any(any)可以处理try语句中抛出的异常。所以,上面的异常表翻译过来就是:如果0到4号指令之间发生了Exception类型的异常,则调用索引为15的指令开始处理异常。如果在指令0和4之间,无论发生什么异常,都调用索引为31的指令(finally代码块开始的地方)如果在指令15和20之间(即catch部分),无论发生什么异常,该指令索引为31的函数被调用。接下来,我们重点关注这部分:怎么样,你找到了吗?就问你厉害不?在源代码中,finally块中只出现一次的输出语句在字节码中出现了三次。finally代码块中的代码被复制两次,分别放在try和catch语句之后。结合异常表的使用,可以达到finally语句一定会执行的效果。以后再也不怕面试官问你为什么finally会被执行了。虽然应该没有面试官会问这么无聊的问题。你问的时候让他从字节码的角度去分析。当然,如果非要给我抬杠,说说System.exit的情况,意义不大。最后,关于finally,我们再讨论一下这个场景:publicclassMainTest{publicstaticvoidmain(String[]args){try{inta=1/0;}最后{System.out.println("final");}}}这个场景没什么好说的,try里抛出异常,触发了finally的输出语句,然后抛出并在控制台打印:如果我在finally里加个return呢?可以看到运行结果没有抛出异常:why?答案就隐藏在字节码中:其实已经一目了然了。右边的finally里面有return,没有throw指令,所以根本没有抛出异常。这也是为什么不建议在finally语句中写return的原因之一。冷知识再补充一个关于异常的冷知识。或者上面的截图。你觉得有点奇怪吗?夜深人静的时候,你有没有想过这样一个问题:程序里没有打印日志的地方,那么谁把控制台的日子打印出来了?谁干的?这个问题很容易回答,你可以猜到是JVM帮我们做了。在哪里?这个问题的答案就隐藏在源码的这个地方。我会打一个断点并为您运行它。当然我建议你也打个断点运行一下:java.lang.ThreadGroup#uncaughtException在这个地方下个断点,根据调用栈可以找到这个地方:java.lang.Thread#dispatchUncaughtException看对该方法的评论:该方法旨在仅由JVM调用。翻译过来就是:这个方法只能被JVM调用。既然源码中是这么说的,那么我们可以找到对应的源码。https://hg.openjdk.java.net/j...在openJdk的thread.cpp源码中,确实找到了调用这个方法的地方:而且这个方法还有一个有趣的用法。看下面的程序和输出结果:我们可以自定义当前线程的UncaughtExceptionHandler,在里面做一些自下而上的操作。是不是有点全局异常处理机制的味道了?好吧,最后一个问题来了:我这样问过,所以答案肯定是不一定的。试想一下,用你的小脑袋好好想想,在什么情况下try里面的代码抛出异常,而外面的catch却抓不到呢?来,看图:没想到吧?这样,外层的catch就不会捕获到异常。你真的要打我吗?别慌,上面的套娃太无聊了。再看看我的代码:publicclassMainTest{publicstaticvoidmain(String[]args){try{ExecutorServicethreadPool=Executors.newFixedThreadPool(1);threadPool.submit(()->{inta=1/0;});}catch(Exceptione){e.printStackTrace();}}}你直接执行,控制台不会有任何输出。看看动画:是不是很神奇?不要惊慌,还有更多。将上面的代码从threadPool.submit修改为threadPool.execute,就会打印异常信息:但是仔细看会发现,虽然打印了异常信息,但并不是因为catch代码块的存在.具体为什么?看这篇文章,之前有详细讲过:《关于多线程中抛异常的这个面试题我再说最后一次!》最后,在这里安排一个赞。感谢阅读,本人坚持原创,欢迎并感谢您的关注。
