06 JVM 是如何处理异常的

在 JAVA 中,异常处理的方式主要是抛出异常和捕获异常。这两大要素共同实现程序控制流的非正常转移。

抛出异常可以分为显示和隐式两种。显示抛出异常的主体是应用程序,它指的是在程序中使用 throw 关键字,手动将异常实例抛出。隐式抛出异常的主题是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。例如数组越界异常。

捕获异常主要设计一下三种代码块:
1:try 代码块,用来标记需要进行异常监控的代码。
2:catch 代码块,跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会自上而下匹配异常处理器,所以前面的 catch 代码块不能覆盖后面的,否则编译器报错。
3:finally 代码块,跟在 catch 代码块之后,用来声明一段必定运行的代码。它的设计是为了避免跳过某些关键的清理代码。比如关闭已经打开的系统资源。

在程序正常执行情况下, finally 代码块会在 try 代码块之后运行。

如果 try 代码块触发异常,异常没有被捕获的情况下,finally 代码块会直接运行,并在运行结束后重新抛出异常。如果该异常被 catch 代码块捕获,finally 代码块则会在 catch 代码块之后运行。在某些情况下,catch 代码块也触发了异常,那么 finally 代码同样会执行,并抛出 catch 代码块触发的异常。如果 finally 代码块也触发了异常,那就中断 finally 代码块,向上抛出异常。

异常的基本概念

在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实现。

Throwable 类有两大直接子类。一个是 Error,涵盖程序不应捕获的异常。当程序触发 Error 的时候,它的执行状态已经无法会发,需要终止线程甚至是终止虚拟机。一个是 Exception ,涵盖程序可能需要捕获并且处理的异常。

在这里插入图片描述

RuntimeException 是 Exception 的一个特殊子类,用来表示程序无法继续执行,但是还能抢救一下的情况,数组越界便是其中一种。

RuntimeException 和 Error 属于 Java 里的非检查异常。其他异常则属于检查异常。在Java 语法中,所有的检查异常都需要程序显示地捕获,或者在方法声明中用 throws 关键字标注。通常情况下,程序自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。

异常实例的构造十分昂贵。在构造异常实例时,Java 虚拟机需要生成该异常的栈轨迹。该曹组会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向的方法的名字,方法所在的类名,文件名,以及在代码中的第几行触发该异常。在生成栈轨迹时,Java 虚拟机会忽略掉异常构造器以及填充栈帧的 Java 方法,直接从新建异常位置开始算起。此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧。

异常实例的构造昂贵,但是却没有做缓存优化。如果做了缓存优化,那么抛出的异常实例对应的栈轨迹并非 throw 语句的位置了,而是第一次新建异常的位置。所以,为了准确的定位到错误的位置,我们往往选择抛出新建异常实例。

Java 虚拟机是如何捕获异常的

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

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

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

如果遍历完异常表的条目未曾匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者中重复上述操作。

finally 代码块的编译比较复杂。当前版本 Java 编译器的做法:复制 finally 代码块内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。

在这里插入图片描述

针对异常执行路径,Java 编译器会生成一个(上图变种2)或者多个(上图变种1)异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块(上图变种1,变种2 中红色 finally block),并且重新抛出捕获的异常。

问题:如果 catch 代码块捕获了异常,并且触发了另一个异常,那么 finally 捕获并且重抛的异常是 catch 代码块触发的新的异常,原本的异常就被忽略了。这对代码调试来说,就不友好了。

Java 7 中引出了 Supressed 异常来解决上面的问题。这个新特性允许开发人员将一个异常附在另一个异常上,这样抛出的异常就可以附带多个异常的信息。

问答
Q:为什么使用异常捕获的代码比较耗费性能

单从 Java 语法上看不出来,但是从 JVM 实现的细节上来看就明白了。构造异常实例,需要生成该异常的栈轨迹。该操作会逐一访问当前线程的栈帧,记录各种调试信息,包括类名,方法名,触发异常的代码行数等等。

Q:finally 是怎么实现无论异常与否都能执行

编译器在编译代码时会复制 finally 代码块放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口处。

Q:finally 中有 ruturn 语句,catch 中抛出的异常会被忽略,为什么

catch 抛出的异常会被 finally 捕获,执行完 finally 后会重新抛出该异常。由于 finally 中有 return 语句,在重新抛出异常之前,代码就已经返回了。

Q:方法的异常表都包含哪些异常

方法的异常表只声明这段代码会被捕获的异常,而且是非检查异常。如果 catch 中有自定义异常,那么异常表中也会包含自定义异常的条目。

Q:检查异常和非检查异常也就是其他书籍中说的编译期异常和运行时异常?

检查异常也会在运行过程中抛出。但是它会要求编译器检查代码有没有显式地处理该异常。非检查异常包括Error和RuntimeException,这两个则不要求编译器显式处理。

总结

本文创作灵感来源于 极客时间 郑雨迪老师的《深入拆解 Java 虚拟机》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

关注本人公众号,第一时间获取最新文章发布,每日更新一篇技术文章。

在这里插入图片描述

<think>首先,用户的问题是关于JVM异常处理机制。用户说:“我想了解JVM如何处理异常 请问JVM 异常处理机制详解”。我需要基于系统指令和用户可见层的要求来构建回答。 系统指令的关键点: - 所有行内数学达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户可见层要求: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 - 参考站内引用:引用[1]和引用[2]是关于JVM异常和类加载的,但用户的问题集中在异常处理上,所以我应该主要关注引用[1],因为它直接相关。 引用内容: - 引用[1]:解释了为什么异常捕获耗费性能,因为构造异常实例时需要生成栈轨迹,这涉及访问栈帧并记录调试信息。 - 引用[2]:关于JVM双亲委派机制,与类加载相关,但不直接涉及异常处理。所以,我应该优先使用引用[1]的信息。 回答结构: - 我需要结构清晰,逐步解释JVM异常处理机制。 - 步骤可能包括:异常的定义、异常抛出、异常捕获、栈轨迹生成、性能影响等。 - 自然地融入引用:在讨论性能时,添加引用标识。 生成相关问题: - 在回答结束后,需要生成相关问题,类似于示例中的“§§相关问题§§”。 LaTeX格式: - 用户的问题中没有数学达式,但如果有必要,我可以使用。例如,如果解释栈帧或内存地址,但可能不需要。保持简单,除必要。 完整回答框架: 1. **引言**:简要介绍JVM异常处理机制。 2. **详细解释**: - 异常的分类:检查异常 vs. 检查异常。 - 异常抛出的过程:当异常发生时,JVM如何响应。 - 异常捕获:try-catch块的工作原理。 - 栈轨迹生成:为什么构造异常实例耗费性能。 - 其他方面:异常处理流程。 3. **性能影响**:基于引用[1],解释构造异常的代价。 4. **总结**:概括关键点。 5. **相关问题**:生成2-3个相关问题。 自然地添加引用: - 例如,在讨论性能时,说“这从代码层面很难理解,不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹。这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息... [^1]”。 相关问题: - 基于主题,生成如: 1. JVM如何处理异常的栈轨迹? 2. 如何优化Java异常处理以提高性能? 3. 异常处理JVM类加载机制的关系? 确保中文回答。</think>### JVM异常处理机制详解 JVMJava虚拟机)的异常处理机制是Java语言错误处理的核心部分,它确保程序在运行时遇到错误时能够优雅地恢复或终止。异常处理涉及异常的抛出、捕获和处理,整个过程由JVM在底层管理。以下我将逐步解释JVM异常处理的关键机制,帮助您全面理解。 #### 1. **异常的分类和定义** - Java异常分为两类:**检查异常(Checked Exceptions)** 和 **检查异常(Unchecked Exceptions)**。检查异常如 `IOException`,必须在编译时处理检查异常如 `NullPointerException`,通常在运行时出现。 - JVM内部,所有异常都是 `java.lang.Throwable` 类的子类对象。当异常发生时,JVM会创建一个异常实例,其中包含错误信息。 #### 2. **异常抛出的过程** - 当代码执行遇到错误(例如,除零操作或空指针访问),JVM会**抛出异常**。这通过 `athrow` 字节码指令实现: - JVM首先检查当前方法是否有异常处理程序(即 `try-catch` 块)。 - 如果没有,JVM会**展开栈帧(Stack Unwinding)**:从当前方法开始,逐层向上查找调用栈,直到找到匹配的异常处理器。 - 例如,在方法调用中: ```java void methodA() { methodB(); // 如果methodB抛出异常JVM从这里开始栈展开 } ``` - JVM维护一个**异常(Exception Table)**,每个方法在编译时生成这个,它记录了 `try-catch` 块的起始、结束位置和异常类型。 #### 3. **异常捕获和处理** - 当JVM找到匹配的 `catch` 块时,它会**捕获异常**: - JVM将控制权转移到 `catch` 块代码。 - 异常实例被传递给 `catch` 块,允许程序处理错误(例如,打印日志或恢复状态)。 - 如果未找到处理器,JVM终止线程并打印**栈轨迹(Stack Trace)**,显示异常发生时的调用路径。 - 处理流程示例: - 假设代码:`try { ... } catch (Exception e) { ... }`。 - JVM使用异常匹配异常类型,如果匹配成功,则执行 `catch` 块;否则继续栈展开。 #### 4. **栈轨迹生成和性能影响** - 构造异常实例时,JVM必须**生成栈轨迹**,这是异常处理中最耗性能的部分。栈轨迹记录了异常触发点的详细信息,包括: - 方法名、类名、文件名。 - 代码行号。 - 调用栈中的每个栈帧(Stack Frame)。 - JVM通过访问当前线程的栈帧来收集这些数据。每个栈帧代一个方法调用,JVM需要遍历整个栈(从当前方法到根方法),并记录调试信息。这个过程是同步的,会阻塞线程执行。 - 为什么耗费性能?这从代码层面很难理解,不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹。这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。虽然具体不清楚JVM实现细节,但是看描述这件事情也是比较费时费力的。[^1] - 性能数据:在频繁抛出异常的场景(如循环中),异常处理可能降低程序速度10-100倍。建议避免在正常流程中使用异常。 #### 5. **其他关键机制** - **finally块处理**:无论是否发生异常JVM都会执行 `finally` 块。这通过修改控制流实现,确保资源清理。 - **异常链**:JVM支持异常嵌套(例如 `e.initCause()`),允许在捕获新异常时保留原始异常信息。 - **与类加载的关系**:异常类(如 `Exception`)在首次使用时由JVM类加载器加载。类加载机制(如双亲委派模型)确保异常类只加载一次,避免重复开销。[^2] #### 总结 JVM异常处理机制通过栈展开、异常和栈轨迹生成,实现了健壮的错误管理。核心优势是隔离错误,但性能开销主要来自栈轨迹的构造。优化建议包括:优先使用条件检查异常、避免在热点代码中抛出异常。理解这些机制有助于编写高效、稳定的Java程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值