异常表用于定位异常处理逻辑的跳转入口,由编译器生成,含start_pc、end_pc、handler_pc和catch_type四字段,通过范围与类型双重匹配决定异常分发。

异常表的作用是定位处理逻辑的跳转入口
JVM在字节码层面不使用类似Java源码中的try-catch-finally嵌套结构,而是依赖方法的异常表(Exception Table)来决定某次异常发生后该跳转到哪条指令继续执行。这个表是编译器在编译阶段生成的,每个表项包含四个字段:start_pc、end_pc、handler_pc 和 catch_type。它们共同定义了一个“监控区间”和对应的“处理入口”。
匹配过程严格遵循范围+类型双重判定
当JVM执行过程中抛出异常时,会按以下顺序检查异常表中的每一条目:
- 当前执行点(即异常抛出位置)是否落在该条目的 start_pc ≤ 异常点 范围内;
- 抛出的异常对象是否为该条目中 catch_type 所指定类或其子类(包括接口实现关系);
- 两个条件同时满足,即匹配成功,JVM将栈顶异常对象压入操作数栈,并跳转至 handler_pc 指向的字节码位置开始执行(通常是
astore指令保存异常引用)。
注意:end_pc 是开区间上限,即不包含自身;若异常发生在 end_pc 处,该条目不参与匹配。多个条目可能同时满足范围条件,但JVM只取异常表中第一个类型匹配的条目,顺序由编译器生成决定,通常与源码中 catch 块书写顺序一致。
finally语句块通过重复插入异常表条目实现覆盖
finally 不对应独立的 catch_type,而是被编译器拆解为多条异常表记录:
- 一条用于正常流程结束后的跳转(
catch_type = null,表示捕获任意异常,包括return、break等非异常控制流); - 若干条分别对应各类已声明的异常类型(如
IOException、Exception),确保无论何种异常或退出方式,都能进入finally块; - 这些条目共享相同的
handler_pc(指向finally的起始指令),但start_pc/end_pc覆盖整个try或catch区域。
这种机制导致 finally 实际上“包裹”了所有可能的出口路径,也是为什么 finally 中的 return 会覆盖 try 或 catch 中的返回值。
常见陷阱:范围重叠与类型擦除的影响
由于异常表匹配基于运行时实际抛出的对象类型和精确PC范围,以下情况易引发意料之外的行为:
-
try块跨方法调用边界时,异常表只覆盖本方法字节码范围:若在
try中调用另一个方法并抛出异常,只要抛出点在本方法的try字节码区间内,就仍受本方法异常表约束; -
泛型异常捕获在字节码层失效:如
catch (List<string> e)</string>在Java语法上非法,但即使类似类型擦除后的catch (List e),其catch_type仍是java/util/List,仅匹配List类型异常,而非其子类(除非显式继承); -
无
catch_type的条目(即finally条目)优先级高于有类型的条目:如果一条catch_type = null的记录范围完全覆盖另一条具体类型的记录,且排在前面,则后者永远无法触发——这在手动编写字节码或使用某些AOP工具时需特别留意。
理解异常表的静态结构与动态匹配规则,是分析JVM异常流向、调试字节码级问题以及正确设计异常处理策略的基础。








