异常原理 | 优雅,永不过时
引言
Java 虚拟机里面的异常使用 Throwable 或其子类的实例来表示,抛异常的本质实际上是程序控制权的一种即时的、非局部(Nonlocal)的转换——从异常抛出的地方转换至处理异常的地方。绝大多数的异常的产生都是由于当前线程执行的某个操作所导致的,这种可以称为是同步的异常。与之相对的,异步异常是指在程序的其他任意地方进行的动作而导致的异常。 Java 虚拟机中异常的出现总是由下面三种原因之一导致的:
1. 虚拟机同步检测到程序发生了非正常的执行情况,这时异常将会紧接着在发生非正常执行情况的字节码指令之后抛出。
- 字节码指令所蕴含的操作违反了 Java 语言的语义,如访问一个元素。
- 类在加载或者链接时出现错误。
- 使用某些资源的时候产生资源限制,例如使用了太多的内存
2. athrow 字节码指令被执行。
3. 由于以下原因,导致了异步异常的出现:
- 调用了 Thread 或者 ThreadGroup 的
- Java 虚拟机实现的内部程序错误。
理解异常
Java异常的底层实现涉及到编译器和虚拟机(JVM)两个层面。包括编译器如何处理异常代码以及虚拟机如何在运行时处理异常。
编译器层面
示例
try {
// 可能引发异常的代码
} catch (SomeException e) {
// 处理 SomeException 的代码
} finally {
// 无论是否发生异常都会执行的代码
}
编译器处理
编译器在将源代码编译成字节码时,会对异常相关的代码进行处理。
- 生成异常表(Exception Table): 编译器会生成一个异常表,其中包含了
try
块的起始和结束位置,以及每个catch
块和finally
块的起始位置。这个表是在字节码中的一部分,用于在运行时确定异常处理逻辑。 - 异常处理代码的插入: 编译器会在可能引发异常的代码周围插入异常处理代码,以确保异常发生时能够跳转到正确的
catch
块或finally
块。
虚拟机层面
JVM实现
JVM在运行时负责执行编译生成的字节码。
- 异常对象的创建: 当在
try
块中的代码引发异常时,JVM会创建一个异常对象,其中包含有关异常的信息,如类型、消息和堆栈跟踪。 - 异常抛出: JVM使用
athrow
指令将异常对象抛出。这通常由throw
关键字触发。 - 异常处理表的使用: 当异常被抛出时,JVM会检查当前方法的异常处理表。它会逐个检查
try
块,看是否匹配抛出的异常。如果找到匹配的catch
块,JVM会跳转到该块的代码执行异常处理逻辑。 - finally 块的执行: 无论是否发生异常,JVM都会执行
finally
块中的代码。这是通过在try
块的最后插入finally
指令实现的。
源码示例
以下是 try-catch-finally
示例
package com.example.demo.exception;
public class TryCatchFinallyExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Caught ArithmeticException: " + e.getMessage());
} finally {
System.out.println("Finally block executed");
}
}
public static int divide(int a, int b) {
return a / b;
}
}
对应的字节码(使用 javap -c
命令查看):
- 先执行编译命令
javac TryCatchFinallyExample.java
- 在执行
javap -c TryCatchFinallyExample
Compiled from "TryCatchFinallyExample.java"
public class com.example.demo.exception.TryCatchFinallyExample {
public com.example.demo.exception.TryCatchFinallyExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: iconst_0
3: invokestatic #2 // Method divide:(II)I
6: istore_1
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
16: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #6 // String Finally block executed
24: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: goto 68
30: astore_1
31: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
34: aload_1
35: invokevirtual #8 // Method java/lang/ArithmeticException.getMessage:()Ljava/lang/String;
38: invokedynamic #9, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
43: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
49: ldc #6 // String Finally block executed
51: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
54: goto 68
57: astore_2
58: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #6 // String Finally block executed
63: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: aload_2
67: athrow
68: return
Exception table:
from to target type
0 19 30 Class java/lang/ArithmeticException
0 19 57 any
30 46 57 any
public static int divide(int, int);
Code:
0: iload_0
1: iload_1
2: idiv
3: ireturn
}
Exception table(异常表)
Exception table
是Java字节码中的一个部分,用于指定方法中的异常处理信息。它描述了在方法执行期间,哪些字节码范围可能抛出异常,以及如何处理这些异常。
我们具体解释 Exception table
部分的含义:
Exception table:
from to target type
0 19 30 Class java/lang/ArithmeticException
0 19 57 any
30 46 57 any
每一行代表一个异常处理条目,它包含以下信息:
- from: 起始字节码索引,表示异常处理的起始位置。
- to: 结束字节码索引,表示异常处理的结束位置。
- target: 处理异常时的目标字节码索引,表示异常被捕获后应该跳转到的位置。
- type: 异常类型,表示应该捕获的异常类型。
第一行: 如果0到19之间,发生了ArithmeticException类型的异常,调用30的位置处理异常。
- 异常处理范围:从字节码索引0到19。
- 异常类型:
java/lang/ArithmeticException
。 - 处理后跳转到字节码索引30。
第二行: 如果0到19之间,发生了任何类型的异常,调用57的位置处理异常。
- 异常处理范围:从字节码索引0到19。
- 异常类型:
any
,表示捕获任何异常。 - 处理后跳转到字节码索引57。
第三行: 如果30到46之间(即catch部分),发生了任何类型的异常,调用57的位置处理异常。
- 异常处理范围:从字节码索引30到46。
- 异常类型:
any
,表示捕获任何异常。 - 处理后跳转到字节码索引57。
通过这个异常表的信息,它告诉Java虚拟机在执行方法时,如果在指定的范围内发生了异常,应该如何处理。每个异常处理条目都包含了异常的类型和处理的范围。如果异常发生在范围内,程序将按照异常处理表中指定的方式进行处理,跳转到相应的目标位置。
再次分析上面的指令
public static void main(java.lang.String[]);
Code:
// try 获取 finally 的代码,如果没有异常发生,则执行输出finally的操作,跳到goto的68位置,执行返回操作。
0: bipush 10
2: iconst_0
3: invokestatic #2 // Method divide:(II)I
6: istore_1
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
16: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #6 // String Finally block executed
24: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: goto 68
// catch 获取 finally代码,如果没有异常发生,则执行输出finally的操作,跳到goto的68位置,执行返回操作。
30: astore_1
31: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
34: aload_1
35: invokevirtual #8 // Method java/lang/ArithmeticException.getMessage:()Ljava/lang/String;
38: invokedynamic #9, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
43: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
49: ldc #6 // String Finally block executed
51: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
54: goto 68
// finally 的代码如果被调用,既有可能是try的异常,也有可能是catch的异常。
57: astore_2
58: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #6 // String Finally block executed
63: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: aload_2
// 如果异常没有被catch捕获,到了这里,执行完finally的语句后,也要把这个异常抛出去,传递给调用处。
67: athrow
68: return
关于指令的解释
bipush 10
:将整数值10推送到操作数栈上。iconst_0
:将整数值0推送到操作数栈上。invokestatic #2
:**(调用静态方法)**调用静态方法divide,传入两个整数参数,并接收一个整数结果。istore_1
:将操作数栈顶的整数值存储到本地变量表的第一个位置。getstatic #3
:获取System.out字段并将其推送到操作数栈上。iload_1
:将第一个局部变量(即从divide方法返回的结果)加载到操作数栈上。invokedynamic #4, 0
:**(调用动态方法)**动态生成并调用一个方法,该方法接受一个整数参数,并返回一个字符串。invokevirtual #5
:**(调用实例方法)**调用PrintStream.println方法,打印出字符串。getstatic #3
:获取System.out字段并将其推送到操作数栈上。ldc #6
:**( 将 int, float 或 String 型常量值从常量池中推送至栈顶。)**将常量池中的字符串"Finally block executed"加载到操作数栈上。invokevirtual #5
:调用PrintStream.println方法,打印出字符串。goto 68
:无条件跳转至第68行。
astore_1
:将操作数栈上的值存储到本地变量表的第一个位置(发生异常时,将异常对象存入这个位置)。getstatic #3
:获取System.out字段并将其推送到操作数栈上。aload_1
:将第一个局部变量(即捕获到的异常对象)加载到操作数栈上。invokevirtual #8
:调用ArithmeticException.getMessage方法,获取异常消息并将其推送到操作数栈上。invokedynamic #9, 0
:动态生成并调用一个方法,该方法接受一个字符串参数,并返回一个字符串。invokevirtual #5
:调用PrintStream.println方法,打印出字符串。getstatic #3
:获取System.out字段并将其推送到操作数栈上。ldc #6
:将常量池中的字符串"Finally block executed"加载到操作数栈上。invokevirtual #5
:调用PrintStream.println方法,打印出字符串。goto 68
:无条件跳转至第68行。
astore_2
:将操作数栈上的值存储到本地变量表的第二个位置(发生异常时,将新的异常对象存入这个位置)。getstatic #3
:获取System.out字段并将其推送到操作数栈上。ldc #6
:将常量池中的字符串"Finally block executed"加载到操作数栈上。invokevirtual #5
:调用PrintStream.println方法,打印出字符串。aload_2
:将第二个局部变量(即新的异常对象)加载到操作数栈上。athrow
: 将栈顶的异常抛出。return
:返回void。
关于指令的操作,大家可以阅读《Java虚拟机规范》- 第 6 章 Java 虚拟机指令集。
总结
当程序执行过程中发生异常时,Java虚拟机(JVM)会按照以下流程处理异常:
- 执行 try : 程序执行到
try
块中的字节码指令。 - 检测异常发生: 当在
try
块中发生异常时,Java虚拟机会检测到异常的发生。 - 异常表匹配: 异常表是在编译时生成的,它包含了每个
try-catch
块的起始位置、结束位置、异常处理器的位置以及期望捕获的异常类型。异常表将被检查以查找与发生的异常类型匹配的处理器。 - 执行字节码指令: 在
try
块中的字节码指令将继续执行,直到异常发生。 - 抛出异常: 当异常发生时,Java虚拟机会创建一个异常对象,并将其抛出。
- 查找匹配的异常处理器: 异常表中的每一项都将被检查,如果发生的异常类型匹配,就会选择相应的异常处理器。
- 遇到异常处理指令: 当匹配到异常处理器时,控制流将跳转到异常处理器的起始位置。这可能涉及到
goto
指令或其他控制流程的改变。 - 异常表中的处理器执行: 执行异常处理器(
catch
块或finally
块)中的字节码指令。在catch
块中,会进行对异常对象的处理,而finally
块则无论是否发生异常都会执行。 - 执行 catch 或 finally: 在异常处理器中执行相应的字节码指令,处理异常或执行清理代码。
- 控制流继续执行: 一旦异常处理完成,程序的控制流将继续执行异常处理代码块之后的代码。
- 点赞
- 收藏
- 关注作者
评论(0)