专栏首页银河系资讯Java虚拟机如何处理异常

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。

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创建并抛出。执行抛出的字节码如下表所示:

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

本文分享自微信公众号 - 银河系1号(gh_19a1776ab1d8),作者:银河系资讯

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-03-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Take Zero-Touch Approach Lock Down IoT Device 采用零接触方式锁定物联网设备

    The demonstrated ability of hackers to penetrate IoT devices says more about the...

    银河1号
  • 程序员50+Java面试题

    大家好!最近,我一直在分享很多Java面试问题和讨论,我的许多读者都要求将它们组合在一起,以便他们可以将它们放在同一页面中并做好准备,这篇文章就是整理的结果。

    银河1号
  • Redis中存储亿级键值对

    迁移系统时,有时你必须建立一个小脚手架。我们最近不得不这样做:在Instagram上,于遗留原因,我们需要将大约3亿张照片映射到创建它们的用户的ID,以便了解要...

    银河1号
  • 请你说说Iterator和ListIterator的区别?

    1)Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。

    剑走天涯
  • 利用IDEA查看和修改spark源码

    经过了两天的摸索,算是初步学会了如何查看和修改spark源码。 大坑 对,这个要写在最前面,那就是注意你的scalaSDK版本!!!!不同的Spark版本支持的...

    用户1148523
  • 构建自定义人脸识别数据集的三种训练方法

    在接下来的几篇文章中,我们将训练计算机视觉+深度学习模型来进行面部识别。在此之前,我们首先需要收集脸部数据集。

    AiTechYun
  • 【JAVA进阶】之类型转换

    如果涉及到日期的转换,则需要在 BeanUtils之前使用注册方法转换一下日期,代码如下

    用户5640963
  • MySQL8.0基础教程 - 事务隔离级别解决之道

    隔离性是事务的基本特性之一,它可以防止数据库在并发处理时出现数据不一致的情况。最严格的情况下,我们可以采用串行化的方式来执行每一个事务,这就意味着事务之间是相互...

    JavaEdge
  • python菜鸟教程 | if elif else 判断

    上一讲主要学习了 if else 内容,本讲将要学习最后一个语句 elif(else if)。

    week
  • Git基础知识(八)--变基

    你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。

    zx钟

扫码关注云+社区

领取腾讯云代金券