他是看了我《神了!异常信息突然就没了?》这篇文章后产生的疑问。
既然是看了我的文章带来的进一步思考,恰巧呢,我又刚好知道。
虽然这类文章看的人少,但是我还是来填个坑。
害,真是暖男石锤了。
异常怎么被抛出的。
先上一个简单代码片段:
运行结果大家都是非常的熟悉。
光看这仅有的几行代码,我们是探索不出来什么有价值的东西。
我们都知道运行结果是这样的,没有任何毛病。
这是知其然。
那么所以然呢?
所以然,就藏在代码背后的字节码里面。
通过 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 尤为明显:
异常表,这个玩意,就是 JVM 拿来处理异常的。
至于这里每个参数的含义是什么,我们直接绕过网上的“二手”资料,到官网上找文档:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.3
看起来英文很多,很有压力,但是不要怕,有我呢,我挑关键的给你 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 指令:
也就是告诉 JVM,如果出异常了,请从这里开始处理。
最后,看 catch_type 参数,对应的是 Exception table 里面的 type。
这里就是程序捕获的异常。
比如我把程序修改为这样,捕获三种类型的异常:
那么编译后的字节码对应的异常表所能处理的 type 就变成了这三个:
别问,问就是语法规定。
具体是啥语法规定呢?
就在异常表的这个地方:
编译器会检查该类是否是 Throwable 或 Throwable 的子类。
关于 Throwable、Exception、Error、RuntimeException 就不细说了,生成一个继承关系图给大家看就行了: