欢迎来到“Under The Hood”第六期。本期我们介绍JVM处理异常的方式,包括如何抛出和捕获异常及相关的字节码指令。但本文不会讨论finally子句,这是下期的主题。你可能需要阅读往期的文章才能更好的理解本文。
异常处理
在程序运行时,异常让你可以平滑的处理意外状况。为了演示JVM处理异常的方式,考虑NitPickyMath类,它提供对整数进行加,减,乘,除以及取余的操作。
NitPickyMath提供的这些操作和Java语言的“+”,“-”,“*”,“/”和“%”是一样的,除了NitPickyMath中的方法在以下情况下会抛出检查型(checked)异常:上溢出,下溢出以及被0除。0做除数时,JVM会抛出ArithmeticException异常,但是上溢出和下溢出不会引发任何异常。NitPickyMath中抛出异常的方法定义如下:
class OverflowException extends Exception { } class UnderflowException extends Exception { } class DivideByZeroException extends Exception { }
NitPickyMath类中的remainder()方法就是一个抛出和捕获异常的简单方法。
static int remainder(int dividend, int divisor) throws DivideByZeroException { try { return dividend % divisor; } catch (ArithmeticException e) { throw new DivideByZeroException(); } }
remainder()方法,只是简单的对当作参数传递进来的2个整数进行取余操作。如果取余操作的除数是0,会引发ArithmeticException异常。remainder()方法捕获这个异常,并重新抛出DivideByZeroException异常。
DivideByZeroException和ArithmeticException的区别是,DivideByZeroException是检查型(checked)异常,而ArithmeticException是非检查(unchecked)型异常。由于ArithmeticException是非检查型异常,一个方法就算会抛出该异常,也不必在其throw子句中声明它。任何Error或RuntimeException异常的子类异常都是非检查型异常。(ArithmeticException就是RuntimeException的子类。)通过捕获ArithmeticException和抛出DivideByZeroException,remainder()方法强迫它的调用者去处理除数为0的可能性,要么捕获它,要么在其throw子句中声明DivideByZeroException异常。这是因为,像DivideByZeroException这种在方法中抛出的检查型异常,要么在方法中捕获,要么在其throw子句中声明,二者必选其一。而像ArithmeticException这种非检查型异常,就不需要去显式捕获和声明。
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 is not used by this catch clause. 5 new #5 < Class DivideByZeroException > // Create and push reference to new object of class // 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()方法的字节码有2个单独的部分。第一部分是该方法的正常执行路径,这部分从第0行开始,到第3行结束。第二部分是从第4行开始,到12行结束的catch子句。
主字节码序列中的irem指令可能会抛出ArithmeticException异常。如果异常发生了,JVM通过在异常表中查找匹配的异常,它会知道要跳转到相应的异常处理的catch子句的字节码序列部分。每个捕获异常的方法,都跟类文件中与方法字节码一起交付的异常表关联。每一个捕获异常的try块,都是异常表中的一行。每行4条信息:开始行号(from)和结束行号(to),要跳转的字节码序列行号(target),被捕获的异常类的常量池索引(type)。remainder()方法的异常表如下所示:
from | to | target | type |
0 | 4 | 4 | < Class java.lang.ArithmeticException > |
上面的异常表表明,行号1到3范围内,ArithmeticException将被捕获。异常表中的“to”下面的结束行号始终比异常捕获的最大行号大1,上表中,结束行号为4,而异常捕获的最大行号是3。行号0到3的字节码序列对应remainder()方法中的try块。“target”列中,是行0到3的字节码发生ArithmeticException异常时要跳转到的目标行号。
如果方法执行过程中产生了异常,JVM会在异常表中查找匹配行。异常表中的匹配行要符合下面的条件:当前pc寄存器的值要在该行的表示范围之内,[from, to),且产生的异常是该行所指定的异常类或其子类。JVM按从上到下的次序查找异常表。当找到了第一个匹配行,JVM把pc寄存器设为新的跳转行号,从此行继续往下执行。如果找不到匹配行,JVM弹出当前栈帧,并重新抛出同一个异常。当JVM弹出当前栈帧时,它会终止当前方法的执行,返回到调用该方法的上一个方法那里。这时,在上一个方法里,并不会继续正常的执行过程,而是抛出同样的异常,促使JVM重新查找该方法的异常表。
Java程序员可以用throw语句抛出像remainder()方法的catch子句中的异常,DivideByZeroException。下表列出了抛出异常的字节码:
OPCODE | OPERAND(S) | DESCRIPTION |
athrow | (none) | pops Throwable object reference, throws the exception |
athrow指令把栈顶元素弹出,该元素必须是Throwable的子类或其自身的对象引用,而抛出的异常类型由栈顶弹出的对象引用所指明。
相关推荐
Exception table 是一个包含异常处理信息的表格,包括可能发生异常的起始点、结束点、目标异常处理者的位置和异常处理者的类信息。当异常发生时,JVM 会在当前方法中查找异常表,是否有合适的处理者来处理。如果当前...
Java虚拟机(JVM)的异常处理是编程中不可或缺的一部分,它确保了程序在遇到错误或异常情况时能够优雅地处理问题,而不是无控制地崩溃。异常处理主要由三个关键部分组成:try、catch和finally代码块。 1. **try ...
1. **异常处理不充分**:程序中对异常的捕获和处理不足,当遇到问题时,没有适当的恢复机制,导致程序崩溃。 2. **资源管理不当**:除了socket,还有其他资源如线程池、数据库连接等,若未有效管理,也可能引发问题...
- **异常处理**:JVM中异常处理机制的设计思路对于提高程序的健壮性和可维护性至关重要。学习JVM可以帮助开发者更好地理解异常抛出与捕获的过程。 5. **促进职业发展** - **面试准备**:在软件工程师面试中,JVM...
Java异常处理是编程中必不可少的部分,用于处理程序运行时可能出现的错误情况。异常是程序执行过程中遇到的非正常状态,可能导致程序中断。Java提供了强大的异常处理机制来确保程序的健壮性和稳定性。 1- 什么是...
Java异常处理是编程中至关重要的一个环节,它用于管理和恢复程序在执行过程中可能出现的问题。Java异常分为两大类:错误(Error)和异常(Exception)。错误通常指的是系统级别的问题,如JVM内部错误或资源耗尽,...
Java异常处理是编程中至关重要的一个环节,它确保了程序的稳定性和健壮性。异常是在程序执行过程中遇到的非正常情况,如果不妥善处理,可能导致Java虚拟机(JVM)的异常停止。Java通过异常类来表示不同类型的异常,...
- JVM支持异常处理框架,通过try-catch-finally语句块来捕获和处理异常。 8. **多线程** - JVM支持多线程并发执行,每个线程有自己的程序计数器、栈和本地方法栈。 9. **动态性与适应性** - JVM允许在运行时...
6. **异常处理**:JVM支持异常处理框架,通过异常表来确定异常发生时的处理流程。 7. **多线程**:JVM内置对多线程的支持,每个线程有自己的程序计数器、本地方法栈和虚拟机栈,共享堆和方法区。 8. **类文件结构*...
try 块中放置可能会发生异常的代码,当异常发生时,try 块抛出系统自动生成的异常对象,然后异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序,并执行 catch 语句。 throws 关键字用于在方法签名中声明...
10. **异常处理**:JVM如何处理运行时异常,以及栈展开的过程。 11. **线程并发**:JVM如何支持多线程,包括线程同步机制如synchronized、Lock等,以及线程池的使用和优化。 通过观看"jvm视频",你可以直观地了解...
JVM的异常处理基于异常表,每个方法的字节码流中包含异常处理信息。`athrow`指令抛出异常,而`catch`块通过`catch`标签来定位异常处理代码。 9. **类加载与反射** 类的加载和初始化涉及到`new`指令以及`...
以上只是JVM众多知识中的一部分,实际上,JVM涉及的领域还包括内存模型、线程管理、异常处理、类加载策略等。理解JVM的工作原理对于编写高效、稳定的Java程序至关重要。通过研究这个压缩包中的资源,你可以更深入地...
- **异常处理**:JVM处理程序运行期间的异常,并根据需要调用相应的异常处理代码。 了解JVM的工作原理对于Java开发者来说至关重要,可以帮助优化代码性能,排查运行时问题,以及理解Java平台无关性的实现机制。...
【异常处理机制】是编程语言中用于处理程序运行时可能出现错误的一种机制,旨在增强程序的稳定性和健壮性。在Java中,异常处理是通过一套独特的语法结构来实现的,主要包括异常类、异常的产生、捕获和处理。 1. **...
10. **异常处理**: - JVM如何处理运行时异常,如栈展开(Stack Unwinding)过程。 这些知识点覆盖了JVM的基础到进阶内容,适合初学者和有一定经验的开发者深入理解Java运行机制。通过学习这份资料,读者可以系统...
Java 中的异常可以是函数中的语句执行时引发的,也可以是程序员通过 throw 语句手动抛出的,只要在 Java 程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE 就会试图寻找异常处理程序来处理异常。...
8. **异常处理指令**:`athrow`用于抛出异常,`catch`和`finally`配合使用来定义异常处理逻辑。 9. **多线程指令**:`monitorenter`和`monitorexit`用于实现synchronized关键字的锁机制。 10. **类和数组操作指令*...
Java异常处理是编程中至关重要的一个方面,它提供了一种有序的方式来处理程序运行时可能出现的问题。异常机制使得异常处理代码与正常的业务逻辑得以分离,从而提高代码的可读性和维护性。 **Java异常架构** Java...