jvm异常处理依赖编译期生成的异常表而非栈帧返回地址:异常发生时,jvm遍历当前方法异常表,按start_pc/end_pc范围和catch_type匹配,成功则跳转handler_pc执行catch块;失败则弹出栈帧向上查找,全程无动态地址计算。

异常抛出时的返回地址不存于栈帧中
方法正常退出时,JVM 会把调用点下一条指令的地址(即 PC 寄存器当前值)作为返回地址,压入新栈帧的“方法出口”区域。但异常退出完全不同:栈帧里不保存异常情况下的返回地址。一旦方法内抛出未捕获异常,JVM 立即放弃使用该栈帧中的任何返回信息,转而启动异常分发机制——核心依据是编译期生成的 exception_handler_table(异常处理器表)。
exception_handler_table 是编译期静态生成的查找索引
这个表不是运行时动态构建的,而是 javac 在编译 .java 源码为 .class 字节码时,根据每个 try-catch 块自动写入方法的 Code 属性中的结构。每条记录包含四个关键字段:
- start_pc / end_pc:标记 try 块覆盖的字节码起始与结束偏移量(左闭右开)
- handler_pc:对应 catch 块第一条指令的字节码地址,即 JVM 跳转后开始执行的位置
-
catch_type:指向常量池中异常类的符号引用索引;值为 0 表示
finally或try-with-resources的隐式 handler
例如,若某异常发生在字节码位置 8,而表中有一条记录 start_pc=5, end_pc=12, handler_pc=25, catch_type=#32,且抛出异常类型能被 #32 所指类或其子类匹配,则 JVM 直接将程序计数器设为 25,跳入 catch 块执行。
JVM 异常分发流程:逐层回溯 + 精确匹配
当 athrow 指令触发异常时,JVM 执行以下步骤:
- 在当前方法的 exception_handler_table 中线性遍历,检查异常发生位置是否落在任一
start_pc到end_pc区间内,且异常类型兼容catch_type - 匹配成功 → 设置 PC =
handler_pc,当前栈帧继续存活,进入 catch 或 finally 块 - 匹配失败 → 弹出当前栈帧,恢复上层调用者的局部变量表和操作数栈,再在其 exception_handler_table 中重复查找
- 若所有栈帧均无匹配项 → 线程终止,由 JVM 默认异常处理器打印堆栈轨迹
整个过程没有“计算返回地址”,也没有修改原有栈帧内容;它纯粹是基于预置表项的地址跳转,因此开销集中于表遍历(通常极短)和栈帧弹出,而非地址推导。
finally 和 try-with-resources 的特殊处理
即使没有显式 catch,finally 块也会在 exception_handler_table 中生成 handler 记录(catch_type = 0)。JVM 保证:只要存在 finally,无论方法是正常 return 还是异常退出,都会插入隐式跳转逻辑,先执行 finally 再决定下一步——这由编译器在字节码层面完成,比如在每个可能退出点后追加 goto handler_pc。所以你看到的“return 后 still run finally”,本质是多个跳转目标被统一调度到同一个 handler_pc,而非返回地址被重写。











