关于java:异常是怎么被处理的这题的答案不在源码里面

37次阅读

共计 5368 个字符,预计需要花费 14 分钟才能阅读完成。

你好呀,我是歪歪。

比方上面这位读者:

他是看了我《神了!异样信息忽然就没了?》这篇文章后产生的疑难。

既然是看了我的文章带来的进一步思考,凑巧呢,我又刚好晓得。

尽管这类文章看的人少,然而我还是来填个坑。

害,真是暖男石锤了。

异样怎么被抛出的。

先上一个简略代码片段:

运行后果大家都是十分的相熟。

光看这仅有的几行代码,咱们是摸索不进去什么有价值的货色。

咱们都晓得运行后果是这样的,没有任何故障。

这是知其然。

那么所以然呢?

所以然,就藏在代码背地的字节码外面。

通过 javap 编译之后,下面的代码的字节码是这样:

咱们次要关注上面局部,字节码指令对应的含意我也在前面正文一下:

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1 // 将 int 型的 1 推送至栈顶
       1: iconst_0 // 将 int 型的 0 推送至栈顶
       2: idiv     // 将栈顶两 int 型数值相除并将后果压入栈顶
       3: istore_1 // 将栈顶 int 型数值存入第二个本地变量
       4: return   // 从以后办法返回 void

别问我怎么晓得字节码的含意的,翻表就行了,这玩意谁背得住啊。

通过字节码,如同也没看出什么玄机来。

然而,你先记着这个样子,马上我给你表演一个变形:

public class MainTest {public static void main(String[] args) {
        try {int a = 1 / 0;} catch (Exception e) {e.printStackTrace();
        }
    }
}

用 try-catch 把代码包裹起来,捕捉一下异样。

再次用 javap 编译之后,字节码变成了这个样子:

能够显著的看到,字节码产生了变动,至多它变长了。

次要还是关注我框起来的局部。

把两种状况的字节码拿来做个比照:

比照一下就很分明了,退出 try-catch 之后,原有的字节码指令一行不少。

没有被框起来的,就是多进去的字节码指令。

而多进去的这部分,其中有个叫做 Exception table 尤为显著:

.png)

异样表,这个玩意,就是 JVM 拿来解决异样的。

至于这里每个参数的含意是什么,咱们间接绕过网上的“二手”材料,到官网上找文档:

https://docs.oracle.com/javas…

看起来英文很多,很有压力,然而不要怕,有我呢,我挑要害的给你 say:

首先 start_pc、end_pc 是一对参数,对应的是 Exception table 外面的 from 和 to, 示意异样的覆盖范围。

比方后面的 from 是 0,to 是 4,代表的异样笼罩的字节码索引就是这个范畴:

0: iconst_1 // 将 int 型的 1 推送至栈顶
1: iconst_0 // 将 int 型的 0 推送至栈顶
2: idiv     // 将栈顶两 int 型数值相除并将后果压入栈顶
3: istore_1 // 将栈顶 int 型数值存入第二个本地变量 

有个细节,不晓得你留神到了没有。

范畴不蕴含 4,范畴区间是这样的 [start_pc, end_pc)。

而至于为什么没有蕴含 end_pc,这个就有点意思了。

拿进去讲讲。

The fact that end_pc is exclusive is a historical mistake in the design of the Java Virtual Machine: if the Java Virtual Machine code for a method is exactly 65535 bytes long and ends with an instruction that is 1 byte long, then that instruction cannot be protected by an exception handler. A compiler writer can work around this bug by limiting the maximum size of the generated Java Virtual Machine code for any method, instance initialization method, or static initializer (the size of any code array) to 65534 bytes.

不蕴含 end_pc 是 JVM 设计过程中的一个历史性的谬误。

因为如果 JVM 中一个办法编译后的代码正好是 65535 字节长,并且以一条 1 字节长的指令完结,那么该指令就不能被异样解决机制所爱护。

编译器作者能够通过限度任何办法、实例初始化办法或动态初始化器生成的代码的最大长度来解决这个谬误。

下面就是官网的解释,反正就是看的似懂非懂的。

没关系,跑个例子就晓得了:

当我代码外面只有一个办法,且长度为 16391 行时,编译进去的字节码长度为 65532。

而通过后面的剖析咱们晓得,一行 a=1/0 的代码,会被编译成 4 行字节码。

那么只有我再加一行代码,就会超出限度,这个时候再对代码进行编译,会呈现什么问题呢?

看图:

间接编译失败,通知你代码过长。

所以你当初晓得了一个知识点:一个办法的长度,从字节码层面来说是有限度的。然而这个限度算是比拟的大,正常人是写不出这样长度的代码的。

尽管这个知识点没啥卵用,然而要是你在工作中真的碰到了一个办法长度成千上万行,即便没有触发字节码长度限度,我也送你一个字:快跑。

接着说下一个参数 handler_pc,对应的是 Exception table 外面的 target。

其实它十分好了解,就是指异样处理程序开始的那条指令对应的索引。

比方这里的 target 是 7,对应的就是 astore_1 指令:

.png)

也就是通知 JVM,如果出异样了,请从这里开始解决。

最初,看 catch_type 参数,对应的是 Exception table 外面的 type。

这里就是程序捕捉的异样。

比方我把程序修改为这样,捕捉三种类型的异样:

那么编译后的字节码对应的异样表所能解决的 type 就变成了这三个:

至于我这里为什么不能写个 String 呢?

别问,问就是语法规定。

具体是啥语法规定呢?

就在异样表的这个中央:

编译器会查看该类是否是 Throwable 或 Throwable 的子类。

对于 Throwable、Exception、Error、RuntimeException 就不细说了,生成一个继承关系图给大家看就行了:

所以,下面的音讯汇总一下:

  • from:可能产生异样的起始点指令索引下标 (蕴含)
  • to:可能产生异样的完结点指令索引下标 (不蕴含)
  • target:在 from 和 to 的范畴内,产生异样后,开始解决异样的指令索引下标
  • type:以后范畴能够解决的异样类信息

晓得了异样表之后,能够答复这个问题了:异样怎么被抛出的?

JVM 通过异样表,帮咱们抛出来的。

异样表外面有啥?

后面我说了, 不再赘述。

异样表怎么用呢?

简略形容一下:

1. 如果出现异常了,JVM 会在以后的办法中去寻找异样表,查看是否该异样被捕捉了。
2. 如果在异样表外面匹配到了异样,则调用 target 对应的索引下标的指令,继续执行。

好,那么问题又来了。如果匹配不到异样怎么办呢?

我在官网文档的这里找到了答案:

https://docs.oracle.com/javas…

它的示例代码是这样的:

而后上面有这样的一句形容:

意思就是如果抛出的值与 catchTwo 的任何一个 catch 子句的参数不匹配,Java 虚拟机就会从新抛出该值,而不调用 catchTwo 的任何一个 catch 子句中的代码。

什么意思?

说白了就是反正我解决不了,我会把异样扔给调用方。

这是编程常识,大家当然都晓得。

然而当常识性的货色,以这样的标准的形容展现在你背后的时候,感觉还是挺微妙的。

当他人问你,为什么是这样的调用流程的时候,你说这是规定。

当他人问你,规定在哪的时候,你能把官网文档拿进去扔他脸上,指着说:就是这里。

尽管,如同没啥卵用。

略微非凡的状况

这一趴再简略的介绍一下有 finally 的状况:

public class MainTest {public static void main(String[] args) {
       try {int a = 1 / 0;} catch (Exception e) {e.printStackTrace();
       } finally {System.out.println("final");
       }
   }
}

通过 javap 编译后,异样表局部呈现了三条记录:

第一条意识,是咱们被动捕捉的异样。

第二三条都是 any,这是啥玩意?

答案在这:

https://docs.oracle.com/javas…

次要看我画线的中央:

一个带有 finally 子句的 try 语句被编译为有一个非凡的异样处理程序,这个异样处理程序能够解决在 try 语句中抛出的(any)任何异样。

所有,翻译一下下面的异样表就是:

  • 如果 0 到 4 的指令之间产生了 Exception 类型的异样,调用索引为 15 的指令,开始解决异样。
  • 如果 0 到 4 的指令之间,不管产生了什么异样,都调用索引为 31 的指令(finally 代码块开始的中央)
  • 如果 15 到 20 的指令之间(也就是 catch 的局部),不管产生了什么异样,都调用索引为 31 的指令。

接着,咱们把眼光放到这一部分:

怎么样,发现了没?就问你神不神奇?

在源码中,只在 finally 代码块呈现过一次的输入语句,在字节码中呈现了三次。

finally 代码块中的代码被复制了两份,别离放到了 try 和 catch 语句的前面。再配合异样表应用,就能达到 finally 语句肯定会被执行的成果。

当前再也不怕面试官问你为什么 finally 肯定会执行了。

尽管应该也没有面试官会问这样无聊的问题。

问起来了,就从字节码的角度给他剖析一波。

当然了,如果你非要给我抬个杠,聊聊 System.exit 的状况,就没多大意义了。

最初,对于 finally,再讨论一下这个场景:

public class MainTest {public static void main(String[] args) {
        try {int a = 1 / 0;} finally {System.out.println("final");
        }
    }
}

这个场景下,没啥说的,try 外面抛出异样,触发 finally 的输入语句,而后接着被抛出去,打印在控制台:

如果我在 finally 外面加一个 return 呢?

能够看到,运行后果外面异样都没有被抛出来:

为什么呢?

答案就藏在字节码外面:

其实曾经高深莫测了。

左边的 finally 外面有 return,并没有 athrow 指令,所以异样基本就没有抛出去。

这也是为什么倡议大家不要在 finally 语句外面写 return 的起因之一。

冷常识

再给大家补充一个对于异样的冷常识吧。

还是下面这个截图。你有没有感觉有一丝丝的奇怪?

夜深人静的时候,你有没有想过这样的一个问题:

程序外面并没有打印日志的中央,那么控制台的日子是谁通过什么中央打印进去的呢?

是谁干的?

这个问题很好答复,猜也能猜到,是 JVM 帮咱们干的。

什么中央?

这个问题的答案,藏在源码的这个中央,我给你打个断点跑一下,当然我倡议你也打个断点跑一下:

java.lang.ThreadGroup#uncaughtException

而在这个中央打上断点,依据调用堆栈顺藤摸瓜能够找到这个中央:

java.lang.Thread#dispatchUncaughtException

看办法上的正文:

This method is intended to be called only by the JVM.

翻译过去就是:这个办法只能由 JVM 来调用。

既然源码外面都这样说了,咱们能够去找找对应的源码嘛。

https://hg.openjdk.java.net/j…

在 openJdk 的 thread.cpp 源码外面的确是找到了该办法被调用的中央:

而且这个办法还有个有意思的用法。

看上面的程序和输入后果:

咱们能够自定义以后线程的 UncaughtExceptionHandler,在外面做一些兜底的操作。

有没有品进去一丝丝全局异样解决机制的滋味?

好了,再来最初一个问题:

我都这样问了,那么答案必定是不肯定的。

你就想想,施展你的小脑袋使劲的想,啥状况下 try 外面的代码抛出了异样,里面的 catch 不会捕捉到?

来,看图:

没想到吧?

这样解决一下,里面的 catch 就捕获不到异样了。

是不是很想打我。

别慌,下面这样套娃多没意思啊。

你再看看我这份代码:

public class MainTest {public static void main(String[] args) {
        try {ExecutorService threadPool = Executors.newFixedThreadPool(1);
            threadPool.submit(()->{int a=1/0;});
        } catch (Exception e) {e.printStackTrace();
        }
    }
}

你间接拿去执行,控制台不会有任何的输入。

来看动图:

是不是很神奇?

不要慌,还有更绝的。

把下面的代码从 threadPool.submit 批改为 threadPool.execute 就会有异样信息打印进去了:

然而你认真看,你会发现,异样信息尽管打印进去了,然而也不是因为有 catch 代码块的存在。

具体是为啥呢?

参见这篇文章,我之前具体讲过的:《对于多线程中抛异样的这个面试题我再说最初一次!》

最初说一句

好了,看到了这里安顿个点赞吧。感谢您的浏览,我保持原创,非常欢送并感谢您的关注。

正文完
 0