异常表是编译器为每个方法自动生成的“异常路由表”,JVM运行时按顺序查表匹配异常位置与类型,命中即跳转执行对应catch块;未命中则向上层传播。

不用讲字节码或JVM规范,新手只需要知道:try-catch不是靠“扫描代码”实现的,而是编译后在方法的结构里悄悄记了一张表——叫“异常表(exception table)”。它像交通指示牌,告诉JVM:“如果从第X行到第Y行之间抛出了Z类异常,请跳转到第Z行去执行catch。”
异常表怎么来的?编译器自动填的
你写:
try {
int a = 10 / b; // 可能抛 ArithmeticException
} catch (ArithmeticException e) {
System.out.println("除零啦");
}
javac编译后,会在该方法的字节码中生成一条异常表记录,类似这样:
- from: 第10行字节码索引(try开始)
- to: 第15行字节码索引(try结束)
- target: 第25行字节码索引(catch块起始)
- type: java/lang/ArithmeticException
运行时JVM不看Java源码,只查这张表——哪个范围、哪种异常、跳哪处理。没匹配上?就向上层调用栈继续找,找不到就崩溃。
立即学习“Java免费学习笔记(深入)”;
为什么catch要从具体到宽泛?
异常表是**按顺序匹配**的。编译器把每个catch块转成一条表项,JVM从上往下查:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 先写
catch (FileNotFoundException e)→ 表里排第一 - 再写
catch (IOException e)→ 表里排第二 - 如果反过来,FileNotFoundException永远匹配不到——它已被父类IOException“提前截胡”
所以语法强制要求:子类异常必须写在父类前面,否则编译报错。
finally为什么总能执行?靠的是“复制指令”
finally不是靠异常表实现的。编译器会把finally里的代码,**原样复制三份**,分别插进:
- try正常结束后的下一行
- 每个catch块末尾
- try/catch内部抛出新异常前(确保清理不被跳过)
这就是为什么finally里写return会覆盖try的返回值——它真被插到了所有出口位置,成了最终执行的那句。
try-with-resources其实是语法糖,底层还是异常表+复制
写:
try (FileInputStream fis = new FileInputStream("a.txt")) {
fis.read();
}
编译后等价于手动加了finally关流,但更聪明:它还会用异常表捕获close()可能抛的异常,并用addSuppressed()把原始异常和关闭异常关联起来,避免后者掩盖前者。
本质上,异常表是JVM快速分发异常的“路由表”,而try-catch是程序员给这张表写规则的方式。理解这点,就不会觉得“异常怎么突然跳到catch去了”——它只是查表、跳转、执行,和if-else一样实在。

















