当前位置: 首页 > news >正文

JVM——JVM 是如何处理异常的?

JVM 是如何处理异常的?

在 Java 编程语言中,异常处理是一种强大的机制,用于应对程序运行时出现的错误和意外情况。而 Java 虚拟机(JVM)作为 Java 程序运行的核心环境,在异常处理过程中扮演着至关重要的角色。下面我们深入探讨 JVM 是如何处理异常的,从异常的基本概念、抛出与捕获机制、异常处理的性能影响,到 Java 7 引入的新特性等多个方面,进行全面而详细的剖析。

异常的基本概念

在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例。Throwable 类有两个直接子类:ErrorException

  • Error :表示程序不应捕获的异常。当程序触发 Error 时,通常意味着程序的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。例如,OutOfMemoryError 表示内存溢出错误,VirtualMachineError 表示虚拟机错误等。这些错误往往是由系统级问题或资源耗尽等问题引起的,应用程序一般无法对其进行有效的处理。

  • Exception :涵盖程序可能需要捕获并且处理的异常。Exception 类又可以分为 RuntimeException 和其他类型的异常(即检查异常)。

RuntimeException 用来表示 “程序虽然无法继续执行,但是还能抢救一下” 的情况,如 ArrayIndexOutOfBoundsException(数组索引越界异常)、NullPointerException(空指针异常)等。RuntimeExceptionError 属于 Java 里的非检查异常(unchecked exception),而其他异常则属于检查异常(checked exception)。

在 Java 语法中,所有的检查异常都需要程序显式地捕获,或者在方法声明中用 throws 关键字标注。通常情况下,程序中自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。这种检查机制可以在编译阶段帮助开发者发现潜在的异常处理问题,提高程序的健壮性。

异常的抛出与捕获机制

(一)抛出异常

抛出异常可分为显式和隐式两种。

  • 显式抛异常 :主体是应用程序,指的是在程序中使用 “throw” 关键字,手动将异常实例抛出。例如下面代码中,当年龄为负数时,程序显式地抛出一个 IllegalArgumentException 异常,提示年龄不能为负数。​

    if (age < 0) {throw new IllegalArgumentException("年龄不能为负数");
    }
  • 隐式抛异常 :主体则是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。例如,Java 虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException):  

     int[] arr = new int[5]; int value = arr[-1]; // 隐式抛出 ArrayIndexOutOfBoundsException

(二)捕获异常

捕获异常涉及如下三种代码块:

  • try 代码块 :用来标记需要进行异常监控的代码。开发者将可能抛出异常的代码放在 try 块中,以便 JVM 对其进行监控。

  • catch 代码块 :跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。例如下面例子中,如果 try 块中的代码抛出了 IOException,则会被第一个 catch 块捕获并处理;如果抛出了其他类型的异常(如 NullPointerException 等),则会被第二个 catch 块捕获并处理。​

    try {// 可能抛出多种异常的代码
    } catch (IOException e) {// 处理 IOException 异常
    } catch (Exception e) {// 处理其他类型的异常
    }
  • finally 代码块 :跟在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源。在程序正常执行的情况下,这段代码会在 try 代码块之后运行。否则,也就是 try 代码块触发异常的情况下:  

    • 如果该异常没有被捕获,finally 代码块会直接运行,并且在运行之后重新抛出该异常。  

    • 如果该异常被 catch 代码块捕获,finally 代码块则在 catch 代码块之后运行。在某些不幸的情况下,catch 代码块也触发了异常,那么 finally 代码块同样会运行,并会抛出 catch 代码块触发的异常。在某些极端不幸的情况下,finally 代码块也触发了异常,那么只好中断当前 finally 代码块的执行,并往外抛异常。 ​

JVM 如何捕获异常

在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。

其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。

举个例子,在以下代码中:

public static void main(String[] args) {try {mayThrowException();} catch (Exception e) {e.printStackTrace();}
}

编译过后,该方法的异常表拥有一个条目。其 from 指针和 to 指针分别为 0 和 3,代表它的监控范围从索引为 0 的字节码开始,到索引为 3 的字节码结束(不包括 3)。该条目的 target 指针是 6,代表这个异常处理器从索引为 6 的字节码开始。条目的最后一列,代表该异常处理器所捕获的异常类型正是 Exception

当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。

如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。

异常处理的性能影响

异常实例的构造十分昂贵。这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。

当然,在生成栈轨迹时,Java 虚拟机会忽略掉异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起。此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧。

既然异常实例的构造十分昂贵,那么在实践中,我们应尽量避免频繁抛出和捕获异常,以免对程序性能造成较大影响。例如,在循环中抛出和捕获异常可能会导致程序运行缓慢。

Java 7 的新特性

(一)Supressed 异常

Java 7 引入了 Supressed 异常来解决异常链问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。

然而,Java 层面的 finally 代码块缺少指向所捕获异常的引用,所以这个新特性使用起来非常繁琐。为此,Java 7 专门构造了一个名为 try-with-resources 的语法糖,在字节码层面自动使用 Supressed 异常。

(二)try-with-resources

try-with-resources 语法糖的主要目的是精简资源打开关闭的用法。在 Java 7 之前,对于打开的资源,我们需要定义一个 finally 代码块,来确保该资源在正常或者异常执行状况下都能关闭。资源的关闭操作本身容易触发异常。因此,如果同时打开多个资源,那么每一个资源都要对应一个独立的 try-finally 代码块,以保证每个资源都能够关闭。这样一来,代码将会变得十分繁琐。

Java 7 的 try-with-resources 语法糖极大地简化了上述代码。程序可以在 try 关键字后声明并实例化实现了 AutoCloseable 接口的类,编译器将自动添加对应的 close() 操作。在声明多个 AutoCloseable 实例的情况下,编译生成的字节码类似于上面手工编写代码的编译结果。与手工代码相比,try-with-resources 还会使用 Supressed 异常的功能,来避免原异常 “被消失”。

例如:

public class Foo implements AutoCloseable {private final String name;public Foo(String name) { this.name = name; }@Overridepublic void close() {throw new RuntimeException(name);}public static void main(String[] args) {try (Foo foo0 = new Foo("Foo0");Foo foo1 = new Foo("Foo1");Foo foo2 = new Foo("Foo2")) {throw new RuntimeException("Initial");}}
}

运行结果:

Exception in thread "main" java.lang.RuntimeException: Initialat Foo.main(Foo.java:18)Suppressed: java.lang.RuntimeException: Foo2at Foo.close(Foo.java:13)at Foo.main(Foo.java:19)Suppressed: java.lang.RuntimeException: Foo1at Foo.close(Foo.java:13)at Foo.main(Foo.java:19)Suppressed: java.lang.RuntimeException: Foo0at Foo.close(Foo.java:13)at Foo.main(Foo.java:19)

(三)多异常捕获

Java 7 还支持在同一 catch 代码块中捕获多种异常。实际实现非常简单,生成多个异常表条目即可。例如:

try {// 可能抛出多种异常的代码
} catch (IOException | SQLException e) {// 处理多种异常
}

实践分析

为了更好地理解 JVM 如何处理异常,我们可以进行一些实践分析。例如,查看以下代码:

public class Foo {private int tryBlock;private int catchBlock;private int finallyBlock;private int methodExit;public void test() {for (int i = 0; i < 100; i++) {try {tryBlock = 0;if (i < 50) {continue;} else if (i < 80) {break;} else {return;}} catch (Exception e) {catchBlock = 1;} finally {finallyBlock = 2;}}methodExit = 3;}
}

我们可以使用 javap -c 命令查看编译后的字节码,分析异常处理的机制。通过观察字节码,我们可以更深入地了解 JVM 如何处理 try-catch-finally 代码块,以及异常表条目的生成和匹配过程。

总结

本文详细探讨了 JVM 是如何处理异常的,包括异常的基本概念、抛出与捕获机制、异常处理的性能影响,以及 Java 7 引入的新特性等内容。通过深入理解这些知识,开发者可以在实际开发中更加合理地使用异常处理机制,提高程序的健壮性和性能。

在实际开发中,我们应尽量遵循以下原则:

  • 避免滥用异常来控制流程,因为异常处理机制相对耗时。

  • 合理使用检查异常和非检查异常,根据实际情况判断是否需要显式捕获或声明抛出异常。

  • 善用 Java 7 的新特性,如 try-with-resources 和多异常捕获,简化代码并提高异常处理的效率。

掌握 JVM 的异常处理机制对于 Java 开发者来说至关重要,它有助于我们编写出更高质量、更可靠的 Java 程序。

相关文章:

  • 双指针算法详解(含力扣和蓝桥杯例题)
  • 《计算机系统结构》考题知识点整理
  • 用可视化学习双指针法
  • 视频编解码学习一之相关学科
  • 数学实验Matlab
  • 自定义Dagster I/O管理器:灵活管理数据输入输出
  • PyTorch_点积运算
  • 隐藏元素的多种方式
  • 2025年4月人工智能发展前沿
  • 【JS逆向】某点数据登录逆向分析
  • 链表的回文结构题解
  • MySQL 比较运算符详解
  • NV189NV195美光固态闪存NV197NV199
  • 学习笔记:Qlib 量化投资平台框架 — FOR DEVELOPERS
  • c++ 函数参数传递
  • HTML与CSS实现风车旋转图形的代码技术详解
  • Windows下调试WebRTC源码
  • diskANN总结
  • 【Linux系统篇】:Linux线程控制基础---线程的创建,等待与终止
  • UDP 通信详解:`sendto` 和 `recvfrom` 的使用
  • 竞彩湃|德甲保级白热化,都灵主帅直面旧主
  • 政府效率部效果不佳?马斯克有意寻求支持,含糊表态部门未来
  • 美的集团一季度净利增长38%,库卡中国机器人接单增超35%
  • 年轻人的事业!6家上海人工智能企业畅想“模范生”新征程
  • 科学家为AI模型设置“防火墙”,以防止被不法分子滥用
  • 专访|200余起诉讼,特朗普上台100天,美国已进入宪政危机