所以,上面的消息汇总一下:
- from:可能发生异常的起始点指令索引下标(包含)
- to:可能发生异常的结束点指令索引下标(不包含)
- target:在from和to的范围内,发生异常后,开始处理异常的指令索引下标
- type:当前范围可以处理的异常类信息
知道了异常表之后,可以回答这个问题了:异常怎么被抛出的?
JVM 通过异常表,帮我们抛出来的。
异常表里面有啥?
前面我说了,不再赘述。
异常表怎么用呢?
简单描述一下:
1.如果出现异常了,JVM 会在当前的方法中去寻找异常表,查看是否该异常被捕获了。
2.如果在异常表里面匹配到了异常,则调用 target 对应的索引下标的指令,继续执行。
好,那么问题又来了。如果匹配不到异常怎么办呢?
我在官网文档的这里找到了答案:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.12
它的示例代码是这样的:
意思就是如果抛出的值与 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/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.13
主要看我画线的地方:
一个带有 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 的原因之一。