转载

Java虚拟机如何处理异常

欢迎来到 Under The Hood专栏 。本专栏旨在让Java开发人员一瞥在运行Java程序底层的神秘机制。本月的文章继续讨论Java虚拟机的字节码指令集,方法是检查Java虚拟机处理异常抛出和捕获的方式,包括相关的字节码。本文不讨论 finally 条款 - 这是下个月的主题。后续文章将讨论字节码系列的其他成员。

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

DivideByZeroExceptionArithmeticException 之间的差别是 DivideByZeroException 是一个 检查 异常,并且 ArithmeticException未经检查 。因为 ArithmeticException 是非受检异常,所以方法不需要在throws子句中声明此异常,即使它可能会抛出它。任何属于 Error 或者 RuntimeException 子类的异常都是非受检异常。( ArithmeticExceptionRuntimeException 的子类。)通过捕获 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 创建并抛出。执行抛出的字节码如下表所示:

Opcode Operand(s) Description
athrow (none) pops Throwable object reference, throws the exception

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底部的文本区域描述了要执行的下一条指令。快乐点击。

英文原文: https://www.javaworld.com/article/2076868/how-the-java-virtual-machine-handles-exceptions.html

原文  http://www.apexyun.com/javaxu-ni-ji-ru-he-chu-li-yi-chang/
正文到此结束
Loading...