Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

问题引入

  • “为什么 finally 块总能执行?JVM 究竟做了什么?”
  • try-catch 在字节码层面是如何实现的?”

1 前置知识

1.1 字节码是什么?

  • Java 代码编译后的中间表示(.class 文件)
  • JVM 执行的指令集,类似汇编语言

1.2 关键指令解析

指令 作用 示例
iconst_0 将 int 型 0 推入栈顶 int a = 0;
idiv 整数除法(可能抛出异常) int b = 10 / 0;
astore_1 将对象引用存储到局部变量表槽位1 Exception e = ...;
getstatic 获取静态字段(如 System.out System.out.println(...);
invokevirtual 调用实例方法(如 println e.getMessage();
athrow 抛出异常对象 throw e;

1.3 异常表(Exception Table)

  • 每个方法编译后生成的“异常处理地图”

  • 结构:

    1
    2
    3
    Exception table:
    from to target type
    0 5 12 java/lang/ArithmeticException
    • from/to:监控的字节码范围(try 块)
    • target:异常发生时跳转的目标位置(catch 块)
    • type:捕获的异常类型(any 表示 finally 块)

2 简单的try-catch-finally Demo

2.1 Demo源码

1
2
3
4
5
6
7
8
9
10
11
12
public class TryCatchFinallyDemo {
public static void main(String[] args) {
try {
int result = 10 / 0; // 触发 ArithmeticException
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("捕获到算术异常: " + e.getMessage());
} finally {
System.out.println("finally块始终执行");
}
}
}

2.2 代码执行流程

  1. try 块:执行 10 / 0 → 抛出 ArithmeticException
  2. catch 块:捕获异常,打印错误信息
  3. finally 块:无论是否异常,都会执行

3 字节码

3.1 常量池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = Methodref #14.#15 // java/io/PrintStream.println:(I)V
#14 = Class #16 // java/io/PrintStream
#15 = NameAndType #17:#18 // println:(I)V
#16 = Utf8 java/io/PrintStream
#17 = Utf8 println
#18 = Utf8 (I)V
#19 = String #20 // finally块始终执行
#20 = Utf8 finally块始终执行
#21 = Methodref #14.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#22 = NameAndType #17:#23 // println:(Ljava/lang/String;)V
#23 = Utf8 (Ljava/lang/String;)V
#24 = Class #25 // java/lang/ArithmeticException
#25 = Utf8 java/lang/ArithmeticException
#26 = Methodref #24.#27 // java/lang/ArithmeticException.getMessage:()Ljava/lang/String;
#27 = NameAndType #28:#29 // getMessage:()Ljava/lang/String;
#28 = Utf8 getMessage
#29 = Utf8 ()Ljava/lang/String;
#30 = InvokeDynamic #0:#31 // #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
#31 = NameAndType #32:#33 // makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
#32 = Utf8 makeConcatWithConstants
#33 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#34 = Class #35 // TryCatchFinallyDemo
#35 = Utf8 TryCatchFinallyDemo
#36 = Utf8 Code
#37 = Utf8 LineNumberTable
#38 = Utf8 main
#39 = Utf8 ([Ljava/lang/String;)V
#40 = Utf8 StackMapTable
#41 = Class #42 // java/lang/Throwable
#42 = Utf8 java/lang/Throwable
#43 = Utf8 SourceFile
#44 = Utf8 TryCatchFinallyDemo.java
#45 = Utf8 BootstrapMethods
#46 = MethodHandle #6:#47 // invokestatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#47 = Methodref #48.#49 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#48 = Class #50 // java/lang/invoke/StringConcatFactory
#49 = NameAndType #32:#51 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#50 = Utf8 java/lang/invoke/StringConcatFactory
#51 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#52 = String #53 // 捕获到算术异常: 
#53 = Utf8 捕获到算术异常: 
#54 = Utf8 InnerClasses
#55 = Class #56 // java/lang/invoke/MethodHandles$Lookup
#56 = Utf8 java/lang/invoke/MethodHandles$Lookup
#57 = Class #58 // java/lang/invoke/MethodHandles
#58 = Utf8 java/lang/invoke/MethodHandles
#59 = Utf8 Lookup

3.2 构造方法

1
2
3
4
5
6
7
8
9
10
public TryCatchFinallyDemo();
descriptor: ()V // 方法签名:无参数,返回void
flags: ACC_PUBLIC // 访问标志:公有方法
Code:
stack=1, locals=1, args_size=1 // 操作数栈深度1,局部变量表大小1,参数个数1(隐式this)
0: aload_0 // 将局部变量0(this)压入栈顶
1: invokespecial #1 // 调用父类Object的构造方法("<init>")
4: return // 方法返回
LineNumberTable:
line 1: 0 // 源码第1行对应字节码偏移0
  • 作用:初始化对象,调用父类 Object 的构造方法。
  • 关键指令:
    • aload_0:加载 this 引用(非静态方法的局部变量0固定为this)。
    • invokespecial #1:调用常量池中 #1 项(Object.<init>)。

3.3 方法入口

1
2
3
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
  • descriptor: 方法签名(参数为String[],返回void
  • flags: 公开静态方法

3.4 字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Code:
stack=2, locals=3, args_size=1
0: bipush 10 // 将10压入栈顶
2: iconst_0 // 将0压入栈顶
3: idiv // 执行除法(10/0),触发ArithmeticException
4: istore_1 // 存储结果(不会执行)
5: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
8: iload_1 // 加载局部变量1(result)
9: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
12: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #19 // String finally块始终执行
17: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: goto 61
23: astore_1
24: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
27: aload_1
28: invokevirtual #26 // Method java/lang/ArithmeticException.getMessage:()Ljava/lang/String;
31: invokedynamic #30, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
36: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
42: ldc #19 // String finally块始终执行
44: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
47: goto 61
50: astore_2
51: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
54: ldc #19 // String finally块始终执行
56: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
59: aload_2
60: athrow
61: return

3.4 try 块逻辑(偏移 0-12)

1
2
3
4
5
6
7
0: bipush        10      // 将10压入栈顶
2: iconst_0 // 将0压入栈顶
3: idiv // 执行除法(10/0),触发ArithmeticException
4: istore_1 // 存储结果(不会执行)
5: getstatic #7 // 获取System.out
8: iload_1 // 加载局部变量1(result)
9: invokevirtual #13 // 调用println打印结果(不会执行)
  • 关键点
    • idiv 触发异常后,直接跳转到 catch 块(偏移23)
    • 偏移4-9的代码被跳过

3.5 catch 块逻辑(偏移23-39)

1
2
3
4
5
6
23: astore_1            // 将异常对象存储到局部变量1(覆盖之前的result)
24: getstatic #7 // 获取System.out
27: aload_1 // 加载异常对象
28: invokevirtual #26 // 调用e.getMessage()
31: invokedynamic #30 // 动态拼接字符串:"捕获到算术异常: "+e.getMessage()
36: invokevirtual #21 // 调用println打印
  • 关键点:
    • astore_1 将异常对象存入局部变量表
    • invokedynamic 优化字符串拼接(JDK9+特性)

3.6 finally 块逻辑(多份复制)

① 正常执行路径(try块未抛异常)

1
2
3
4
12: getstatic     #7      // System.out
15: ldc #19 // 加载"finally块始终执行"
17: invokevirtual #21 // 调用println
20: goto 61 // 跳过catch块,直接返回

② catch块执行后

1
2
3
4
39: getstatic     #7      // System.out
42: ldc #19 // 加载"finally块始终执行"
44: invokevirtual #21 // 调用println
47: goto 61 // 方法返回

③ 未捕获异常路径

1
2
3
4
5
6
7
50: astore_2            // 存储未捕获的异常
51: getstatic #7 // System.out
54: ldc #19 // 加载"finally块始终执行"
56: invokevirtual #21 // 调用println
59: aload_2 // 重新加载异常
60: athrow // 抛出异常
61: return // 方法返回
  • 关键点:
    • finally 代码被复制到 3个位置,确保任何路径都会执行
    • athrow 保证未捕获异常在 finally 后继续传播

3.7 异常表(核心机制)

1
2
3
4
5
Exception table:
from to target type
0 12 23 Class java/lang/ArithmeticException // try范围:0-12 → catch块23
0 12 50 any // try范围 → finally块50
23 39 50 any // catch范围 → finally块50
  • 作用:
    • 第一行:捕获 ArithmeticException,跳转到 catch
    • 第二/三行:any 类型强制跳转到 finally 块(覆盖所有路径)

总结

1 JVM如何实现finally的“必定执行”

  • 代码复制:将 finally 块逻辑插入到所有退出路径(try/catch/未捕获异常)
  • 异常表控制:通过 any 类型强制跳转
  • 资源保障:即使未捕获异常,也会先执行 finally 再终止程序

评论