目录

Java中的try-catch在jvm层面是怎么做的

Java中的try-catch在jvm层面是怎么做的?

简单描述

java中的try-catch通过 异常表栈展开 来实现

异常表(exception-table)

每个方法的字节码中都有一个异常表,用于记录try-catch块的 作用范围 对应的异常处理逻辑

异常表的每个条目包含以下信息:

起点,终点,处理代码的位置,捕获异常的类型

起点(start_pc):try块的 起始指令偏移量

终点(end_pc):try块的 结束指令偏移量 (不包含该指令)

处理代码位置(handler_pc):catch块的 第一条指令偏移量

捕获的异常类型(catch_type):要捕获的异常类(如java/lang/Exception),若为0表示捕获所有异常(finally块)

字节码结构

Exception table:
   start  end  handler  type
   0      10   13       java/io/IOException
   0      10   20       java/lang/Exception

异常处理流程

抛出异常,从异常表判断异常是否在处理逻辑内(也就是是否被try-catch{}包围),

代码中抛出异常时,JVM会执行以下步骤:

创建异常对象 :实例化抛出的异常(如 new IOException()

查找异常表 :从当前方法的异常表中,按顺序匹配以下条件:

异常抛出的位置是否在某个条目的 [start_pc, end_pc) 范围内。

抛出的异常是否是 catch_type 的子类(或自身)

跳转到处理代码

若找到匹配条目,跳转到 handler_pc 执行 catch

若未找到,触发 栈展开 :弹出当前栈帧,回到调用者方法重复上述过程。 栈展开 确保异常沿调用链向上传播,直到被处理或终止线程

未捕获异常 :若所有栈帧均未处理异常,线程 终止并打印堆栈跟踪


finally块的实现

finally 块的核心是:无论 try 或 catch 块中是否抛出异常或提前返回, finally 中的代码必须执行

为了实现这一点,JVM 的编译器(如 javac)在生成字节码时,会通过两种机制来确保 finally 的执行

finally块通过两种方式实现:

代码复制 :编译器将 finally 代码复制到 trycatch 块的所有退出路径(包括 return 或异常抛出之后)。

异常表条目兜底 :若 finally 需要处理异常退出,会生成一个 catch_type=0 的条目,捕获所有异常并执行 finally 代码,之后重新抛出异常


代码复制

编译器会将 finally 块中的代码复制到所有可能的退出路径,包括:

try 块正常结束后的退出路径。

catch 块处理完异常后的退出路径。

try 或 catch 块中的 return、break、continue 语句之前

java代码

public void example() {
    try {
        System.out.println("try");
    } catch (Exception e) {
        System.out.println("catch");
    } finally {
        System.out.println("finally");
    }
}

编译后的字节码逻辑

// try 块
L0:
    System.out.println("try");
    // 复制 finally 代码到 try 块末尾
    System.out.println("finally");
    return;

// catch 块
L1:
    System.out.println("catch");
    // 复制 finally 代码到 catch 块末尾
    System.out.println("finally");
    return;

// 异常表条目(自动处理异常后的 finally)
Exception table:
    start=L0, end=L0, handler=L1, type=Exception

关键点:

finally 的代码会被复制到 try 和 catch 的末尾 ,确保正常流程下一定会执行

如果 try 或 catch 中有 return,编译器会先执行 finally 代码,再执行 return


异常表兜底(处理未捕获的异常)

如果 try 或 catch 块中抛出了未被捕获的异常,或者有 throw 语句,JVM 会通过异常表跳转到 finally 代码,执行后再重新抛出异常。

异常表条目

编译器会生成一个 特殊的异常表条目用于捕获所有类型的异常(catch_type=0)

确保任何未处理的异常都会先执行 finally,再继续传播异常

java代码

public void example() {
    try {
        throw new IOException();
    } finally {
        System.out.println("finally");
    }
}

字节码的异常表会生成如下头目

Exception table:
    start=L0, end=L1, handler=L2, type=0  // type=0 表示捕获所有异常

对应的执行流程:

  1. try 块抛出 IOException
  2. JVM 查找异常表,发现 type=0 的条目(匹配所有异常)。
  3. 跳转到 handler=L2finally 代码的位置)执行 System.out.println("finally")
  4. 重新抛出异常 ,继续栈展开

假设代码中有 try 和 finally,但没有 catch:

try {
    throw new Exception();
} finally {
    System.out.println("finally");
}

执行步骤:

  1. try 块抛出异常,JVM 创建异常对象。
  2. 直接查找当前方法的异常表 ,找到 catch_type=0 的条目,跳转到 finally 代码。
  3. 执行 finally 块中的代码。
  4. 重新抛出异常 ,由外层调用者处理