本专栏的目的是使Java开发人员了解运行中的Java程序下单击和旋转的神秘机制。本月的文章通过检查Java虚拟机处理异常引发和捕获的方式(包括相关的字节码),继续讨论Java虚拟机的字节码指令集。本文不讨论finally
条款-这是下个月的主题。随后的文章将讨论字节码家族的其他成员。
例外情况
异常使您可以顺利处理程序运行时发生的意外情况。为了演示Java虚拟机处理异常的方式,请考虑一个名为
NitPickyMath
提供了对整数执行加,减,乘,除和除法的方法。
NitPickyMath
执行这些数学运算的方式与Java的“ +”,“-”,“ *”,“ /”和“%”运算符提供的常规运算相同,除了
NitPickyMath
在溢出,下溢和被零除的情况下引发检查的异常。Java虚拟机将抛出一个
ArithmeticException
整数除以零,但不会在溢出和下溢时引发任何异常。方法抛出的异常
NitPickyMath
定义如下:
class OverflowException extends Exception {}
class UnderflowException extends Exception {}
class DivideByZeroException extends Exception {}
捕获并引发异常的简单方法是remainder
class方法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
。
DivideByZero
和ArithmeticException
例外之间的区别在于,DivideByZeroException
是已检查的异常而未检查的ArithmeticException
是。由于未经检查,因此即使方法可能抛出异常,也无需在throws子句中声明此异常。作为或的子类的任何异常都未经检查。(是的子类。)通过捕获然后抛出,该方法强制其客户端通过捕获它或在自己的throws子句中声明来解决被零除的异常的可能性。这是因为检查异常,例如ArithmeticException
Error
RuntimeException
ArithmeticException
RuntimeException
ArithmeticException
DivideByZeroException
remainder
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>
上面的异常表表明ArithmeticException
捕获了从pc偏移量0到3(含3)的数据。表中标签为“ to”的try块的端点值始终比捕获异常的最后一个pc偏移量大一个。在这种情况下,端点值列出为4,但是捕获到异常的最后一个pc偏移为3。此范围(从零到三)(包括零),对应于在的try块内实现代码的字节码序列remainder
。表格中列出的目标是pc偏移量(如果ArithmeticException
在pc偏移量0与3之间(含零)之间抛出an,则跳至该偏移量)。
如果在方法执行期间引发异常,则Java虚拟机将在异常表中搜索匹配的条目。如果当前程序计数器在条目指定的范围内,并且抛出的异常类是条目指定的异常类(或者是指定异常类的子类),则异常表条目匹配。Java虚拟机按照条目在表中出现的顺序搜索异常表。找到第一个匹配项后,Java虚拟机将程序计数器设置为新的pc偏移位置,并在该位置继续执行。如果未找到匹配项,则Java虚拟机将弹出当前堆栈帧并抛出相同的异常。当Java虚拟机弹出当前堆栈帧时,它有效地中止了当前方法的执行,并返回到调用此方法的方法。但是,它没有在先前的方法中正常继续执行,而是在该方法中引发了相同的异常,这使Java虚拟机经历了相同的搜索该方法的异常表的过程。
Java程序员可以使用throw语句引发异常,例如a的catch(ArithmeticException
)子句中的语句remainder
,在该语句中DivideByZeroException
创建并引发a。下表显示了进行抛出的字节码:
操作码 | 操作数 | 描述 |
---|---|---|
投掷 | (没有) | 弹出Throwable对象引用,引发异常 |
所述athrow指令从堆栈中弹出顶部字和希望它是一个对象,它是一个子类的引用Throwable
(或Throwable
本身)。抛出的异常是由弹出的对象引用定义的类型。
Play Ball!:Java虚拟机模拟
下面的小程序演示了执行字节码序列的Java虚拟机。模拟中的字节码序列由
Java语言
为了
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
方法将永远循环。每四分之一的循环中,玩球都会抛出a Ball
并接住它,只是因为这很有趣。因为try块和catch子句都在无尽的while循环中,所以乐趣永不停止。局部变量i从0开始,并在每次循环中递增。当if
语句true
为时,每当我等于3 时都会发生,Ball
则抛出异常。
Java虚拟机检查异常表并发现确实存在适用的条目。条目的有效范围是2到15,包括端值,并且在pc偏移量12处引发了异常。条目捕获的异常是class Ball
,并且引发的异常是class Ball
。有了这种完美匹配,Java虚拟机将抛出的异常对象压入堆栈,并在pc偏移量19处继续执行。catch子句仅将int i重置为0,然后循环重新开始。
要驱动仿真,只需按下“ Step”按钮。每按一次“ Step”按钮,将使Java虚拟机执行一个字节码指令。要重新开始仿真,请按下“复位”按钮。要使Java虚拟机重复执行字节码而无需您再进行任何同轴电缆连接,请按“运行”按钮。然后,Java虚拟机将执行字节码,直到按下“停止”按钮为止。小程序底部的文本区域描述了要执行的下一条指令。点击愉快。
Bill Venners从事专业软件编写已有12年了。他位于硅谷,以Artima Software Company的名义提供软件咨询和培训服务。多年来,他为消费电子,教育,半导体和人寿保险行业开发了软件。他在许多平台上用多种语言编程:各种微处理器上的汇编语言,Unix上的C,Windows上的C ++,Web上的Java。他是McGraw-Hill出版的《 Java虚拟机内部》一书的作者。长按识别
本文分享自微信公众号 - 黑帽子技术(SNJYYNJY2020)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。