- 作者:老汪软件技巧
- 发表时间:2024-11-30 00:03
- 浏览量:
引言
Java 虚拟机里面的异常使用 Throwable 或其子类的实例来表示,抛异常的本质实际上是程序控制权的一种即时的、非局部(Nonlocal)的转换——从异常抛出的地方转换至处理异常的地方。绝大多数的异常的产生都是由于当前线程执行的某个操作所导致的,这种可以称为是同步的异常。与之相对的,异步异常是指在程序的其他任意地方进行的动作而导致的异常。 Java 虚拟机中异常的出现总是由下面三种原因之一导致的:
1. 虚拟机同步检测到程序发生了非正常的执行情况,这时异常将会紧接着在发生非正常执行情况的字节码指令之后抛出。
2. athrow 字节码指令被执行。
3. 由于以下原因,导致了异步异常的出现:
理解异常
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."":()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
每一行代表一个异常处理条目,它包含以下信息:
第一行: 如果0到19之间,发生了ArithmeticException类型的异常,调用30的位置处理异常。
第二行: 如果0到19之间,发生了任何类型的异常,调用57的位置处理异常。
第三行: 如果30到46之间(即catch部分),发生了任何类型的异常,调用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
关于指令的解释
关于指令的操作,大家可以阅读《Java虚拟机规范》- 第 6 章 Java 虚拟机指令集。
总结
当程序执行过程中发生异常时,Java虚拟机(JVM)会按照以下流程处理异常:
执行 try : 程序执行到 try 块中的字节码指令。检测异常发生: 当在 try 块中发生异常时,Java虚拟机会检测到异常的发生。异常表匹配: 异常表是在编译时生成的,它包含了每个 try-catch 块的起始位置、结束位置、异常处理器的位置以及期望捕获的异常类型。异常表将被检查以查找与发生的异常类型匹配的处理器。执行字节码指令: 在 try 块中的字节码指令将继续执行,直到异常发生。抛出异常: 当异常发生时,Java虚拟机会创建一个异常对象,并将其抛出。查找匹配的异常处理器: 异常表中的每一项都将被检查,如果发生的异常类型匹配,就会选择相应的异常处理器。遇到异常处理指令: 当匹配到异常处理器时,控制流将跳转到异常处理器的起始位置。这可能涉及到 goto 指令或其他控制流程的改变。异常表中的处理器执行: 执行异常处理器(catch 块或 finally 块)中的字节码指令。在 catch 块中,会进行对异常对象的处理,而 finally 块则无论是否发生异常都会执行。执行 catch 或 finally: 在异常处理器中执行相应的字节码指令,处理异常或执行清理代码。控制流继续执行: 一旦异常处理完成,程序的控制流将继续执行异常处理代码块之后的代码。