阿里P7浅析Java虚拟机如何处理异常

46次阅读

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

Exceptions
Exceptions 允许您顺利处理程序运行时发生的意外情况。要演示 Java 虚拟机处理异常的方式,请考虑一个名为 NitPickyMath 的类。它提供了对整数执行加法,减法,乘法,除法和余数的方法。NitPickyMath 在溢出,下溢和被零除的条件下抛出已检查的异常。Java 虚拟机将在整数除零上抛出一个 ArithmeticException,但不会在溢出和下溢上抛出任何异常。方法抛出的异常定义如下:
class OverflowException extends Exception {
}
class UnderflowException extends Exception {
}
class DivideByZeroException extends Exception {
}
捕获和抛出异常的简单方法是 remainder 类的方法 NitPickyMath:
static int remainder(int dividend, int divisor)
throws DivideByZeroException {
try {
return dividend % divisor;
}
catch (ArithmeticException e) {
throw new DivideByZeroException();
}
}
该 remainder 方法仅在传递两个 int 参数时执行余数运算。如果余数运算的除数为零,则余数运算抛出一个 ArithmeticException。这个方法捕获了这个 ArithmeticException 并抛出一个 DivideByZeroException。DivideByZeroException 和 ArithmeticException 之间的差别是 DivideByZeroException 是一个 检查 异常,并且 ArithmeticException 是 未经检查。因为 ArithmeticException 是非受检异常,所以方法不需要在 throws 子句中声明此异常,即使它可能会抛出它。任何属于 Error 或者 RuntimeException 子类的异常都是非受检异常。(ArithmeticException 是 RuntimeException 的子类。)通过捕获 ArithmeticException 然后抛出 DivideByZeroException,该 remainder 方法强制其客户端处理除零异常的可能性,通过捕获它或在自己的 throws 子句中声明 DivideByZeroException。这是因为已检查的异常,例如 DivideByZeroException,抛出方法必须由方法捕获或在方法的 throws 子句中声明。未经检查的异常(例如 ArithmeticException,不需要在 throws 子句中捕获或声明)。
javac 为该 remainder 方法生成以下字节码序列:
The main bytecode sequence for remainder:
0 iload_0 // Push local variable 0 (arg passed as divisor)
1 iload_1 // Push local variable 1 (arg passed as dividend)
2 irem // Pop divisor, pop dividend, push remainder
3 ireturn // Return int on top of stack (the remainder)
The bytecode sequence for the catch (ArithmeticException) clause:
4 pop // Pop the reference to the ArithmeticException
// because it isn’t used by this catch clause.
5 new #5 <Class DivideByZeroException>
// Create and push reference to new object of class
// DivideByZeroException.
DivideByZeroException
8 dup // Duplicate the reference to the new
// object on the top of the stack because it
// must be both initialized
// and thrown. The initialization will consume
// the copy of the reference created by the dup.
9 invokenonvirtual #9 <Method DivideByZeroException.<init>()V>
// Call the constructor for the DivideByZeroException
// to initialize it. This instruction
// will pop the top reference to the object.
12 athrow // Pop the reference to a Throwable object, in this
// case the DivideByZeroException,
// and throw the exception.
该 remainder 方法的字节码序列有两个独立的部分。第一部分是该方法的正常执行路径。这部分从 pc 偏移 0 到 3。第二部分是 catch 子句,它从 pc 偏移 4 到 12。
主字节码序列中的 irem 指令可能会抛出一个 ArithmeticException。如果发生这种情况,Java 虚拟机知道通过查找表中的异常来跳转到实现 catch 子句的字节码序列。捕获异常的每个方法都与一个异常表相关联,该异常表在类文件中与方法的字节码序列一起传递。每个 try 块捕获的每个异常在异常表中都有一个条目。每个条目都有四条信息:起点和终点,要跳转到的字节码序列中的 pc 偏移量,以及正被捕获的异常类的常量池索引。remainder 类的 NitPickyMath 方法的异常表如下所示:
Exception table:
from to target type
0 4 4 <Class java.lang.ArithmeticException>
上面的异常表指示从 pc 偏移 0 到 3(包括 0),表示 ArithmeticException 将被捕获的范围。在标签“to”下面的表中列出的是 try 块的端点值,它总是比捕获异常的最后一个 pc 偏移量多一。在这种情况下,端点值列为 4,捕获到异常的最后一个 pc 偏移量为 3。此范围(包括 0 到 3)对应于在 remainder 的 try 块内实现代码的字节码序列。如果 ArithmeticException 在 pc 偏移量为 0 和 3 之间(包括 0 和 3)之间抛出,则表中列出的 ”to” 就是跳转到的 pc 偏移量。
如果在执行方法期间抛出异常,Java 虚拟机将在异常表中搜索匹配的条目。如果当前程序计数器在条目指定的范围内,并且抛出的异常类是由条目指定的异常类(或者是指定异常类的子类),则异常表条目匹配。Java 虚拟机按照条目在表中的显示顺序搜索异常表。找到第一个匹配项后,Java 虚拟机会将程序计数器设置为新的 pc 偏移位置并继续执行。如果未找到匹配项,Java 虚拟机将弹出当前堆栈帧并重新抛出相同的异常。当 Java 虚拟机弹出当前堆栈帧时,它有效地中止当前方法的执行并返回调用此方法的方法。但是,不是在前一个方法中继续正常执行,而是在该方法中抛出相同的异常,这会导致 Java 虚拟机经历搜索该方法的异常表的相同过程。
Java 程序员可以使用 throw 语句抛出异常,例如 remainder 中的一个子句 catch(ArithmeticException),其中一个 DivideByZeroException 创建并抛出。执行抛出的字节码如下表所示:

athrow 指令从堆栈中弹出顶部字节,并且会认为它是一个 Throwable 子类的引用(或 Throwable 本身)。抛出的异常是弹出对象引用定义的类型。
Play Ball!: a Java virtual machine simulation
下面的 applet 演示了一个执行一系列字节码的 Java 虚拟机。模拟中的字节码序列由 javac 生成。
类的 playBall 方法如下所示:
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;
}
}
}
}
javac 为该 playBall 方法生成的字节码如下所示:
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>
该 playball 方法永远循环。每四次循环,playball 抛出 Ball 并抓住它,只是因为它很有趣。因为 try 块和 catch 子句都在无限循环中,所以乐趣永远不会停止。局部变量 i 从 0 开始,每次递增递增循环。当 if 语句出现 true 时,每次 i 等于 3 时都会发生 Ball 异常,抛出异常。
Java 虚拟机检查异常表并发现确实存在适用的条目。条目的有效范围是 2 到 15(包括两者),异常在 pc 偏移 12 处抛出。条目捕获的异常是类 Ball,抛出的异常是类 Ball。鉴于这种完美匹配,Java 虚拟机将抛出的异常对象推送到堆栈,并继续在 pc 偏移 19 处执行 catch 子句,这里仅将 int i 重置为 0,并且循环重新开始。
要驱动模拟,只需按“步骤”按钮。每次按下“Step”按钮都会使 Java 虚拟机执行一个字节码指令。要开始模拟,请按“重置”按钮。要使 Java 虚拟机重复执行字节码而不需要进一步操作,请按“运行”按钮。然后,Java 虚拟机将执行字节码,直到按下“停止”按钮。applet 底部的文本区域描述了要执行的下一条指令。快乐点击。
不仅仅是虚拟机,针对 Java 的 Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分布式、高并发、性能调优、微服务等架构技术,我这边都给大伙儿找了点资料,希望能帮助到有需要的人,记得私信我哟
怎么领取 → 进粉丝群:963944895,私聊管理员即可
写在最后:
既然看到这里了,觉得笔者写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

正文完
 0