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

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 程序。

http://www.xdnf.cn/news/3678.html

相关文章:

  • 传奇各版本迭代时间及内容变化,屠龙/嗜魂法杖/逍遥扇第一次出现的时间和版本
  • Linux53 百度网盘运行(下载devtoolset11后仍提示stdc++3.0.29缺失 计划用docker容器隔离运行,计划后续再看)
  • 鼠标悬浮特效:常见6种背景类悬浮特效
  • Docker与WSL2如何清理
  • 一篇文章看懂web服务
  • NV203NV207SSD固态闪存NV208NV213
  • NoxLucky:个性化动态桌面,打造独一无二的手机体验
  • 用docker ffmpeg测试视频vmaf分数,很快不用编译
  • C#VisionMaster算子二次开发(非方案版)
  • NocoDB:开源的 Airtable 替代方案
  • operator 可以根据需要重载 == 运算符进行比较
  • 《告别试错式开发:TDD的精准质量锻造术》
  • 【quantity】7 角度单位模块(angle.rs)
  • 电脑RGB888P转换为JPEG方案 ,K230的RGB888P转换为JPEG方案
  • CGI(Common Gateway Interface)协议详解
  • 【AI面试准备】TensorFlow与PyTorch构建缺陷预测模型
  • AtCoder AT_abc404_g [ABC404G] Specified Range Sums
  • ​​信息泄露:网站敏感文件泄漏的隐形危机与防御之道​
  • 前端面试每日三题 - Day 23
  • 泰迪杯特等奖案例学习资料:基于时空图卷积网络的城市排水系统水位精准重建与异常检测
  • Power Query精通指南2:数据转换——透视/逆透视/分组、横向纵向合并数据、条件判断、处理日期时间
  • 如何设计抗Crosstalk能力强的PCB镀穿孔
  • Linux 进程间通信(IPC)详解
  • 【计算机视觉】目标检测:yoloV1~yoloV11项目论文及对比
  • 【信息系统项目管理师-论文真题】2011上半年论文详解(包括解题思路和写作要点)
  • LVGL -文本显示 英文、中文
  • MaC QT 槽函数和Lambda表达式
  • Leetcode刷题记录29——矩阵置零
  • 【JavaScript】性能优化:打造高效前端应用
  • 数据赋能(212)——质量管理——统一性原则