Java 代码中通过使用 try-catch-finally 块来对异常进行捕获/处理。但是对于 JVM 来说,是如何处理 try/catch 代码块与异常的呢?
实际上 Java代码在进行编译时,编译器会在代码后附加一个异常表,以实现try块出现异常后能进入对应的异常处理程序执行。
- 如果在方法执行期间抛出异常,Java 虚拟机会在异常表中搜索匹配的条目。
- 如果当前PC程序计数器在条目指定的范围内,并且抛出的异常类是条目指定的异常类(或者是指定异常类的子类),则异常表条目匹配。
- Java 虚拟机按照条目在表中出现的顺序搜索异常表。当找到第一个匹配项时,Java 虚拟机将程序计数器设置为新的 pc 偏移位置并在那里继续执行。如果未找到匹配项,Java 虚拟机将弹出当前堆栈帧并重新抛出相同的异常。
JVM对异常表的约定
在 JVM 规范中,对 Exception table 有以下几个约定:1.
Exception table 的结构:
在 JVM 规范中,Exception table 被定义为一张表格,由多行记录组成。每一行记录用于描述一个代码块,其中包含了代码块的起始地址、结束地址、异常处理程序的程序计数器(PC)值以及异常类类型。
2.
Exception table 的字节码偏移值:
在 JVM 规范中,Exception table 中的每个记录都包含了字节码偏移值(Bytecode offset)和行号信息,这些信息用于告诉 JVM 在哪个字节码偏移值处发生了异常。这一信息在调试 Java 代码时十分有用。
3.
Exception table 列表的顺序:
在 JVM 规范中,Exception table 中的代码块记录必须按照字节码地址从低到高排序。这个顺序确保了在 JVM 查找代码块和异常处理程序时能够正确有效。
4.
Exception table 的匹配逻辑:
在 JVM 规范中,当 JVM 发生异常时,会遍历 Exception table 列表,按行依次匹配当前 PC 计数器和代码块的起始和结束字节码偏移值,以确定当前发生异常所在的代码块和异常处理程序。
Exception table 是 JVM 中非常重要的数据结构之一,为 JVM 提供了一种有效、可靠的异常处理机制。在 Java 编程中,Exception table 可以帮助开发者更好更快地调试和解决问题,保证 Java 程序的可靠性和稳定性。
模拟JVM的执行过程
class Ball extends Exception {
}
class Pitcher {
private static Ball ball = new Ball();
static void playBall() {
int i = 0;
while (true) {
try {
if (i % 4 == 3) {
throw ball;
}
++i;
}
catch (Ball b) {
i = 0;
}
}
}
}
编译后通过javap -v进行反编译,找到playBall Java方法对应的Code指令:
0 iconst_0 // Push constant 0
1 istore_0 // Pop into local var 0: int i = 0;
// The try block starts here (see exception table, below).
2 iload_0 // Push local var 0
3 iconst_4 // Push constant 4
4 irem // Calc remainder of top two operands
5 iconst_3 // Push constant 3
6 if_icmpne 13 // Jump if remainder not equal to 3: if (i % 4 == 3) {
// Push the static field at constant pool location #5,
// which is the Ball exception itching to be thrown
9 getstatic #5 <Field Pitcher.ball LBall;>
12 athrow // Heave it home: throw ball;
13 iinc 0 1 // Increment the int at local var 0 by 1: ++i;
// The try block ends here (see exception table, below).
16 goto 2 // jump always back to 2: while (true) {}
// The following bytecodes implement the catch clause:
19 pop // Pop the exception reference because it is unused
20 iconst_0 // Push constant 0
21 istore_0 // Pop into local var 0: i = 0;
22 goto 2 // Jump always back to 2: while (true) {}
Exception table:
from to target type
2 16 19 <Class Ball>
对于每个 catch 块捕获的异常,异常表都有一个条目。每个条目有四个信息:
- from:可能发生异常的起始点指令索引下标(包含)
- to:可能发生异常的结束点指令索引下标(不包含)
- target:在from和to的范围内,发生异常后,开始处理异常的指令索引下标
- type:当前范围可以处理的异常类信息
基于异常表条目,可以判断出
- try块,对应PC偏移范围的 2~15
- catch块,对应PC偏移范围的 19~21
异常表中,覆盖范围区间是左开右闭 [from, to),为什么没有包含右边界,这个就有点意思了
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.
start_pc、end_pc 是一对参数,对应的是 Exception table 里面的 from 和 to,表示异常的覆盖范围。
不包含 end_pc 是 JVM 设计过程中的一个历史性的错误。
在Java中,一个方法的长度从字节码层面来说是有限制的。具体来说,Java虚拟机规范定义了一个方法的字节码长度不能超过65535个字节,也就是64KB。
这个限制是由于Java虚拟机规范中方法表结构的设计所决定的。方法表结构中有一个字段code_length用于表示方法的字节码长度,这个字段是一个16位的无符号整数,因此最大值为65535。
因为如果 JVM 中一个方法编译后的代码正好是 65535 字节长,并且以一条 1 字节长的指令结束,那么该指令就不能被异常处理机制所保护。存在边界问题,因此异常的覆盖范围定为左开右闭。
示例中,Pitcher#playball方法会一直循环;每经过四次循环,playball 就会抛出Ball并catch住。
因为 try 块和 catch 子句都在while(true) 循环中,所以永远不会停止。文章来源:https://www.toymoban.com/news/detail-471014.html
局部变量i
从 0 开始,每次循环递增。当if语句为 时true,即每次i
等于 3 时都会抛出异常。
Java 虚拟机检查异常表,发现确实有匹配的条目。条目的有效范围是从 2 到 15,包括了在 pc 偏移量 12 处抛出异常。条目对应能处理的异常类型class 是Ball,抛出异常的 class 也是Ball。鉴于这种完美匹配,Java 虚拟机将抛出的异常对象压入堆栈,并在 pc 偏移量 19 处继续执行。
catch 子句只是将int i
重置为 0,然后循环重新开始。文章来源地址https://www.toymoban.com/news/detail-471014.html
到了这里,关于Java中的异常表(Exception table)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!