异常体系与JVM机制
# 04.异常体系与JVM机制
# 目录介绍
- 12.1 开篇疑问
- 12.2 异常体系全景
- 12.3 JVM异常处理机制
- 12.4 异常的性能代价
- 12.5 异常设计的哲学争论
- 12.6 try-with-resources原理
- 12.7 异常处理最佳实践
- 12.8 JDK版本中异常的演进
- 12.9 常见面试深度问题
- 12.10 总结与核心要点
# 12.1 开篇疑问
疑惑:try-catch 到底有没有性能开销?为什么 Java 有 Error 和 Exception 两种异常?受检异常到底是好设计还是坏设计?finally 块是怎么保证一定执行的?异常创建为什么那么慢?
答疑:异常处理是 Java 语言的核心机制之一。大多数人只知道 try-catch-finally 的语法,但很少深入到 JVM 层面理解异常是如何被处理的。本篇将从字节码、异常表、栈展开等底层机制出发,揭示异常处理的真正原理。
# 12.2 异常体系全景
# 12.2.1 Throwable家族树
Throwable
├── Error(严重错误,不应捕获)
│ ├── OutOfMemoryError ← 堆/元空间/直接内存溢出
│ ├── StackOverflowError ← 栈溢出(递归过深)
│ ├── NoClassDefFoundError ← 类定义找不到
│ ├── VirtualMachineError ← JVM 内部错误
│ │ ├── InternalError
│ │ └── UnknownError
│ └── LinkageError ← 类链接错误
│ ├── ClassCircularityError
│ ├── IncompatibleClassChangeError
│ └── UnsatisfiedLinkError
└── Exception(可处理的异常)
├── RuntimeException(非受检异常,编程错误)
│ ├── NullPointerException ← 最常见
│ ├── ArrayIndexOutOfBoundsException
│ ├── ClassCastException
│ ├── IllegalArgumentException
│ │ └── NumberFormatException
│ ├── IllegalStateException
│ ├── ConcurrentModificationException
│ ├── UnsupportedOperationException
│ └── ArithmeticException
└── 受检异常(必须处理)
├── IOException
│ ├── FileNotFoundException
│ └── SocketException
├── SQLException
├── ClassNotFoundException
├── InterruptedException
├── CloneNotSupportedException
└── ReflectiveOperationException
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
# 12.2.2 Error与Exception的设计区分
疑惑:为什么要区分 Error 和 Exception?直接用 Exception 不行吗?
论证:这是对"可恢复"与"不可恢复"的设计分离。
| 类型 | 含义 | 应对策略 | 示例 |
|---|---|---|---|
| Error | JVM 或系统级别的严重问题 | 无法恢复,不应捕获 | OOM、SOF |
| Exception | 程序逻辑中的可处理异常 | 可以且应该捕获处理 | IO、SQL |
// 错误示范:捕获 Error
try {
recursion();
} catch (StackOverflowError e) {
// 不应该捕获!栈溢出意味着程序逻辑有根本性问题
// 即使捕获了,程序状态可能已经不一致
}
// 正确:捕获 Exception
try {
readFile(path);
} catch (IOException e) {
// 文件不存在是可预期的情况,可以优雅处理
log.warn("文件不存在: " + path);
useDefaultConfig();
}
// 特例:某些场景需要捕获 OOM
try {
byte[] bigArray = new byte[1024 * 1024 * 1024]; // 1GB
} catch (OutOfMemoryError e) {
// 可以捕获,但要确保 catch 块内不会再分配大量内存
// 这种用法需要非常小心
log.error("内存不足", e);
// 释放一些缓存,降级处理
}
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
# 12.2.3 受检异常与非受检异常
| 类型 | 继承自 | 编译器要求 | 代表 |
|---|---|---|---|
| 受检异常 | Exception(非RuntimeException) | 必须 try-catch 或 throws | IOException |
| 非受检异常 | RuntimeException | 不强制处理 | NullPointerException |
设计意图:受检异常强制调用者考虑异常情况,避免"偷懒"忽略错误。非受检异常代表编程错误(bug),不应该靠 catch 来"解决"。
// 受检异常的示例
public byte[] readFile(String path) throws IOException {
// IOException 是受检异常,调用者必须处理
return Files.readAllBytes(Paths.get(path));
}
// 非受检异常的示例
public void process(String input) {
if (input == null) {
throw new IllegalArgumentException("input不能为null");
// RuntimeException 的子类,不需要声明 throws
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 12.2.4 常见异常分类速查
编程错误类(修复代码):
| 异常 | 原因 | 解决方案 |
|---|---|---|
| NullPointerException | 对 null 调用方法/访问字段 | 空值检查 / Optional |
| ArrayIndexOutOfBoundsException | 数组下标越界 | 检查边界 |
| ClassCastException | 类型转换失败 | instanceof 检查 |
| IllegalArgumentException | 方法参数不合法 | 参数校验 |
| ConcurrentModificationException | 遍历时修改集合 | 用 Iterator.remove() |
| StackOverflowError | 递归过深 | 检查递归终止条件 |
环境资源类(处理异常):
| 异常 | 原因 | 解决方案 |
|---|---|---|
| IOException | IO 操作失败 | 重试 / 降级 / 报错 |
| FileNotFoundException | 文件不存在 | 检查路径 / 创建文件 |
| SocketException | 网络连接异常 | 重连 / 断路器 |
| SQLException | 数据库操作异常 | 重试 / 检查SQL |
| OutOfMemoryError | 内存溢出 | 增大堆 / 排查泄漏 |
# 12.3 JVM异常处理机制
# 12.3.1 异常表的字节码结构
try-catch 在字节码层面不是通过 if-else 实现的,而是通过异常表(Exception Table):
public void example() {
try {
int a = 1 / 0; // 可能抛出 ArithmeticException
} catch (ArithmeticException e) {
e.printStackTrace();
}
}
2
3
4
5
6
7
编译后的字节码中包含异常表:
字节码:
0: iconst_1
1: iconst_0
2: idiv // 可能抛异常的指令
3: istore_1
4: goto 12 // 正常执行,跳过 catch 块
7: astore_1 // catch 块开始
8: aload_1
9: invokevirtual #2 // e.printStackTrace()
12: return
Exception table:
from to target type
0 4 7 ArithmeticException
2
3
4
5
6
7
8
9
10
11
12
13
14
含义:如果字节码偏移量 0~4 之间抛出 ArithmeticException,跳转到偏移量 7 处继续执行。
关键点:try 块不存在时(没有异常抛出),异常表不会被查询,几乎零开销。开销只在异常真正发生时才产生。
# 12.3.2 多重catch的异常表
try {
riskyOperation();
} catch (IOException e) {
handleIO(e);
} catch (SQLException e) {
handleSQL(e);
} catch (Exception e) {
handleGeneral(e);
}
2
3
4
5
6
7
8
9
编译后的异常表:
Exception table:
from to target type
0 8 11 IOException
0 8 22 SQLException
0 8 33 Exception
2
3
4
5
匹配规则:异常表从上到下顺序匹配。JVM 找到第一个匹配的 handler 就跳转执行。这就是为什么 catch 块需要从具体到一般排列——如果 Exception 放在最前面,后面的 catch 永远不会执行。
JDK 7 的多异常捕获:
// JDK 7+ 支持 multi-catch
try {
riskyOperation();
} catch (IOException | SQLException e) {
// 编译器为两种异常生成同一个 handler
handle(e);
}
// 异常表:两条记录指向同一个 target
// Exception table:
// from to target type
// 0 8 11 IOException
// 0 8 11 SQLException
2
3
4
5
6
7
8
9
10
11
12
13
# 12.3.3 异常匹配与栈展开
当异常抛出时,JVM 执行以下步骤:
1. 在当前方法的异常表中查找匹配的 handler
→ 遍历异常表,检查:
a. 异常发生的 PC 在 [from, to) 范围内?
b. 抛出的异常是 type 的实例?(instanceof 检查)
→ 找到:跳转到 handler(target)执行
→ 没找到:进入步骤2
2. 弹出当前栈帧(栈展开 Stack Unwinding)
→ 回到调用者方法
→ 在调用者的异常表中继续查找
→ 如此递归,直到找到 handler 或到达栈底
3. 到达栈底仍未找到
→ 线程的 UncaughtExceptionHandler 处理
→ 默认行为:打印异常堆栈,线程终止
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void methodA() {
try {
methodB();
} catch (Exception e) { // handler 在这里
// 处理异常
}
}
void methodB() {
methodC(); // 没有 try-catch
}
void methodC() {
throw new RuntimeException("boom"); // 异常在这里抛出
}
// 栈展开过程:
// 1. methodC 异常表没有匹配 → 弹出 methodC 栈帧
// 2. methodB 异常表没有匹配 → 弹出 methodB 栈帧
// 3. methodA 异常表匹配成功 → 跳转到 catch 块
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
栈展开的开销:栈展开需要遍历调用栈的每一层,每层都要查询异常表。调用栈越深,开销越大。这是异常性能代价的重要来源之一。
# 12.3.4 finally的实现原理
finally 在字节码层面的实现方式是代码复制——编译器将 finally 块的代码复制到每个可能的退出路径上:
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
cleanup();
}
2
3
4
5
6
7
编译后等价于:
try 正常路径:
执行 try 块
复制一份 finally 代码(cleanup())
return 1
catch 路径:
执行 catch 块
复制一份 finally 代码(cleanup())
return 2
异常未捕获路径:
复制一份 finally 代码(cleanup())
重新抛出异常(athrow)
2
3
4
5
6
7
8
9
10
11
12
13
字节码验证:
Exception table:
from to target type
0 4 10 Exception // try → catch
0 4 20 any // try → finally(异常未捕获时)
10 14 20 any // catch → finally(catch中又抛异常时)
2
3
4
5
type = any(或 null)表示匹配任何异常,这就是 finally 的实现——无论是否异常都会执行。
# 12.3.5 finally的经典陷阱
陷阱1:finally 中的 return 会覆盖 try/catch 中的 return
public int test() {
try {
return 1;
} finally {
return 2; // 覆盖了 try 的 return 1
}
}
// 返回 2,不是 1
// 原因:finally 的代码在 return 之前执行
// 如果 finally 中有 return,直接返回,try 的 return 被丢弃
2
3
4
5
6
7
8
9
10
11
陷阱2:finally 中修改返回值——基本类型 vs 引用类型
// 基本类型:finally 修改不生效
public int testPrimitive() {
int result = 1;
try {
return result; // return 前已将 result 的值保存到临时变量
} finally {
result = 2; // 修改的是局部变量,不影响已保存的返回值
}
}
// 返回 1(不是2)
// 引用类型:finally 修改生效(修改对象内容)
public List<Integer> testReference() {
List<Integer> list = new ArrayList<>();
try {
list.add(1);
return list; // return 前保存的是引用(指针)
} finally {
list.add(2); // 通过引用修改了对象内容
}
}
// 返回 [1, 2](finally 的修改对调用者可见)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
陷阱3:finally 吞掉异常
public void testSwallow() throws Exception {
try {
throw new RuntimeException("original");
} finally {
throw new RuntimeException("from finally");
// 原始异常 "original" 被吞掉了!
// 调用者只能看到 "from finally"
}
}
// try-with-resources 解决了这个问题(Suppressed 异常)
2
3
4
5
6
7
8
9
10
# 12.4 异常的性能代价
# 12.4.1 创建异常的开销
创建异常对象的主要开销在于填充栈轨迹(fillInStackTrace()):
// 创建异常时,JVM 需要遍历整个调用栈,记录每一层的类名、方法名、行号
// 调用栈越深,开销越大
Throwable t = new Exception("test");
// 内部调用: fillInStackTrace() → 遍历几十甚至上百层栈帧
2
3
4
性能对比(调用栈深度 20,执行 100 万次):
| 操作 | 耗时 | 倍数 |
|---|---|---|
| 创建普通对象 | ~10ms | 1x |
| 创建异常对象(含栈轨迹) | ~1000ms | 100x |
| 创建异常对象(禁用栈轨迹) | ~15ms | 1.5x |
# 12.4.2 fillInStackTrace源码分析
// Throwable 构造器会调用 fillInStackTrace
public class Throwable {
public Throwable(String message) {
fillInStackTrace(); // 关键!
this.detailMessage = message;
}
// native 方法,遍历调用栈
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null || backtrace != null) {
fillInStackTrace(0); // JVM 内部实现
stackTrace = UNASSIGNED_STACK;
}
return this;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
优化:禁用栈轨迹
// 方式1:重写 fillInStackTrace
public class FastException extends RuntimeException {
@Override
public synchronized Throwable fillInStackTrace() {
return this; // 不填充栈轨迹,性能接近普通对象
}
}
// 方式2:JDK 7+ 的 protected 构造器
public class FastException extends RuntimeException {
public FastException(String message) {
// writableStackTrace=false → 不记录栈轨迹
super(message, null, true, false);
}
}
// 方式3:单例异常(如 Netty 中的优化)
// 预先创建好异常实例,避免重复创建
public static final ClosedChannelException INSTANCE = new ClosedChannelException();
static {
INSTANCE.setStackTrace(new StackTraceElement[0]); // 清空栈轨迹
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 12.4.3 异常与正常控制流对比
// 反面教材:用异常做流程控制
try {
int i = 0;
while (true) {
array[i++]; // 用 ArrayIndexOutOfBoundsException 终止循环
}
} catch (ArrayIndexOutOfBoundsException e) { }
// 正确做法:正常控制流
for (int i = 0; i < array.length; i++) {
process(array[i]);
}
// 性能对比(数组长度 1000,执行 10000 次)
// 正常控制流: ~5ms
// 异常控制流: ~500ms(慢 100 倍)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
结论:try 块本身几乎无开销(异常表只在异常发生时查询),但异常的创建和栈展开代价很高。绝不要用异常做正常的流程控制。
# 12.4.4 JIT对异常的优化
// JIT 编译器会对频繁抛出的异常做优化
// 如果同一个位置反复抛出同一类型的异常
// JIT 可能会省略 fillInStackTrace(因为栈轨迹总是一样的)
// 这个优化由 -XX:+OmitStackTraceInFastThrow 控制(默认开启)
// 生产环境中,如果看到异常没有栈轨迹,可能就是这个原因
// 排查建议:如果需要完整栈轨迹用于排查问题
// 可以临时关闭这个优化:
// -XX:-OmitStackTraceInFastThrow
2
3
4
5
6
7
8
9
10
# 12.5 异常设计的哲学争论
# 12.5.1 受检异常之争
支持者(Java 设计者的初衷):
- 强制调用者处理可预见的异常情况
- 方法签名明确告知可能的异常
- 编译器帮助检查遗漏的异常处理
反对者(Kotlin、C#、Go 的选择):
- 受检异常导致代码冗余(大量样板 try-catch)
- 底层异常沿调用链传播,破坏了封装性
- 开发者往往偷懒写空 catch 块,比不处理更危险
// 受检异常的"链式污染"
void methodA() throws IOException { methodB(); }
void methodB() throws IOException { methodC(); }
void methodC() throws IOException { readFile(); }
// 每一层都要声明 throws,一个底层异常影响整个调用链
2
3
4
5
与 Lambda 的不兼容:
// 受检异常与 Stream API 格格不入
List<String> paths = Arrays.asList("a.txt", "b.txt", "c.txt");
paths.stream()
.map(path -> Files.readAllBytes(Paths.get(path))) // 编译错误!
// IOException 是受检异常,但 Function 接口不声明 throws
// 丑陋的解决方案
paths.stream()
.map(path -> {
try {
return Files.readAllBytes(Paths.get(path));
} catch (IOException e) {
throw new UncheckedIOException(e); // 包装为非受检异常
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 12.5.2 各语言的异常设计对比
| 语言 | 异常机制 | 受检异常 |
|---|---|---|
| Java | try-catch-finally + 受检异常 | 有 |
| Kotlin | try-catch-finally | 无(所有异常都是非受检的) |
| C# | try-catch-finally | 无 |
| Go | 返回值(error)+ panic/recover | 无异常机制 |
| Rust | Result<T,E> + panic! | 无异常机制 |
| Swift | try-catch (throws) | 有(但更轻量) |
现代趋势:即使在 Java 生态中,Spring 等框架也倾向于使用非受检异常。Kotlin 完全抛弃了受检异常。函数式编程偏好用 Result/Either 类型代替异常。
# 12.5.3 异常的正确使用原则
- 异常用于异常情况,不用于正常控制流
- 优先使用标准异常:
IllegalArgumentException、IllegalStateException、NullPointerException等 - 提供有意义的异常消息:包含出错的参数值、期望条件等
- 不要忽略异常:空 catch 块是最危险的代码
- 异常转译而非暴露底层实现:上层不该知道底层用的是 SQL 还是文件
- 尽早失败(Fail Fast):在方法入口检查参数,不要等到深处才抛异常
// 反面教材:空 catch 块
try {
riskyOperation();
} catch (Exception e) {
// 什么都不做——最危险的代码!
// 异常被吞掉,问题悄无声息地发生
}
// 正确做法
try {
riskyOperation();
} catch (Exception e) {
log.error("操作失败: " + context, e); // 至少记录日志
throw new ServiceException("操作失败", e); // 或转译后重新抛出
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 12.6 try-with-resources原理
# 12.6.1 传统资源释放的问题
// 传统方式——繁琐且容易遗漏
InputStream in = null;
try {
in = new FileInputStream("file");
// 使用 in
} catch (IOException e) {
// 处理异常
} finally {
if (in != null) {
try {
in.close(); // close 也可能抛异常
} catch (IOException e) {
// 吞掉 close 的异常?还是覆盖原异常?
// 两种做法都有问题
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 12.6.2 try-with-resources编译器转换
// try-with-resources——简洁安全
try (InputStream in = new FileInputStream("file")) {
// 使用 in
} catch (IOException e) {
// 处理异常
}
// in 自动关闭
2
3
4
5
6
7
编译器生成的等价代码:
// 编译器实际生成的逻辑(简化)
InputStream in = new FileInputStream("file");
Throwable primaryException = null;
try {
// 使用 in
} catch (Throwable t) {
primaryException = t;
throw t;
} finally {
if (in != null) {
if (primaryException != null) {
try {
in.close();
} catch (Throwable closeEx) {
// close 的异常作为 suppressed 附加到原异常
primaryException.addSuppressed(closeEx);
}
} else {
in.close(); // 正常关闭
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
要求:资源类必须实现 AutoCloseable 接口。
// 自定义可自动关闭的资源
public class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("资源已释放");
}
}
// 多资源按声明的逆序关闭
try (InputStream in = new FileInputStream("input");
OutputStream out = new FileOutputStream("output")) {
// 使用 in 和 out
}
// 先关闭 out,再关闭 in
2
3
4
5
6
7
8
9
10
11
12
13
14
# 12.6.3 Suppressed异常机制
// Suppressed 异常解决了"close 异常覆盖原异常"的问题
try (MyResource resource = new MyResource()) {
throw new RuntimeException("primary");
// resource.close() 也抛出异常
} catch (Exception e) {
System.out.println("主异常: " + e.getMessage());
// "primary"
// 获取被抑制的异常
Throwable[] suppressed = e.getSuppressed();
for (Throwable t : suppressed) {
System.out.println("被抑制: " + t.getMessage());
}
}
// 输出:
// 主异常: primary
// 被抑制: close exception
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 12.7 异常处理最佳实践
# 12.7.1 异常转译与异常链
// 异常转译:将底层异常转为上层语义的异常
public User findUser(int id) {
try {
return userDao.selectById(id);
} catch (SQLException e) {
// 不要暴露 SQL 异常给上层
throw new UserNotFoundException("用户不存在: " + id, e);
// 保留原始异常作为 cause,便于排查
}
}
// 异常链:通过 getCause() 追溯根因
try {
findUser(1);
} catch (UserNotFoundException e) {
System.out.println(e.getMessage()); // 用户不存在: 1
System.out.println(e.getCause()); // java.sql.SQLException: ...
System.out.println(e.getCause().getCause()); // 可能还有更底层的原因
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 12.7.2 全局异常处理
// 1. 线程的未捕获异常处理器
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
log.error("线程 {} 未捕获异常", thread.getName(), throwable);
// 告警、记录日志、优雅降级等
});
// 2. Spring MVC 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusiness(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handleUnknown(Exception e) {
log.error("未知异常", e);
return Result.fail(500, "系统异常");
}
}
// 3. CompletableFuture 的异常处理
CompletableFuture.supplyAsync(() -> riskyOperation())
.exceptionally(throwable -> {
log.error("异步任务失败", throwable);
return defaultValue;
});
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
# 12.7.3 自定义异常体系设计
// 基础业务异常
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public int getCode() { return code; }
}
// 具体业务异常
public class UserNotFoundException extends BusinessException {
public UserNotFoundException(String userId) {
super(404, "用户不存在: " + userId);
}
}
public class InsufficientBalanceException extends BusinessException {
public InsufficientBalanceException(BigDecimal balance, BigDecimal amount) {
super(400, String.format("余额不足: 余额=%s, 需要=%s", balance, amount));
}
}
// 错误码枚举
public enum ErrorCode {
USER_NOT_FOUND(10001, "用户不存在"),
INSUFFICIENT_BALANCE(10002, "余额不足"),
SYSTEM_ERROR(50000, "系统异常");
private final int code;
private final String message;
// ...
}
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
# 12.7.4 日志与异常的配合
// 正确的异常日志记录
// 1. 记录完整异常信息(包括栈轨迹)
log.error("操作失败, userId={}", userId, exception);
// 注意:exception 作为最后一个参数,SLF4J 会自动打印栈轨迹
// 2. 不要只记录 message
log.error("操作失败: " + exception.getMessage()); // 错误!丢失了栈轨迹
// 3. 不要重复记录
// 底层已经记录了日志的异常,上层不要再记录
// 要么记录日志,要么抛出异常,不要两者都做
catch (Exception e) {
log.error("失败", e); // 记录了
throw e; // 又抛出了 → 上层还会再记录一次
}
// 正确做法:
catch (Exception e) {
throw new ServiceException("操作失败", e); // 只抛出,让上层统一记录
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 12.8 JDK版本中异常的演进
| JDK版本 | 异常相关特性 |
|---|---|
| JDK 1.0 | 基础异常体系 (try-catch-finally) |
| JDK 1.4 | 异常链 (Throwable.getCause()) |
| JDK 7 | try-with-resources, multi-catch, Suppressed 异常 |
| JDK 9 | try-with-resources 支持 effectively final 变量 |
| JDK 14 | NullPointerException 改进消息(-XX:+ShowCodeDetailsInExceptionMessages) |
| JDK 16 | 默认启用 NullPointerException 详细消息 |
JDK 14+ NPE 详细消息:
// JDK 14 之前
a.b.c.d = 1;
// NullPointerException(哪个是 null?不知道!)
// JDK 14+
// NullPointerException: Cannot read field "c" because "a.b" is null
// 精确告诉你哪个引用是 null
2
3
4
5
6
7
JDK 9 try-with-resources 增强:
// JDK 7: 必须在 try() 中声明资源
InputStream in = new FileInputStream("file");
try (InputStream in2 = in) { // 必须再声明一个变量
// ...
}
// JDK 9+: 支持 effectively final 变量
InputStream in = new FileInputStream("file");
try (in) { // 直接使用已声明的变量
// ...
}
2
3
4
5
6
7
8
9
10
11
# 12.9 常见面试深度问题
Q1:try-catch 有性能开销吗?
try 块本身几乎无开销(只是异常表中增加一条记录)。开销在于:异常对象的创建(fillInStackTrace 遍历调用栈)和异常的匹配(栈展开遍历调用链)。
Q2:finally 一定会执行吗?
几乎一定,除了以下情况:
System.exit()被调用- JVM 崩溃(如 JNI 错误导致 JVM 进程终止)
- 线程被
Thread.stop()(已废弃) - 所在线程是守护线程,JVM 正常退出时
Q3:Error 可以被 catch 吗?
语法上可以(Error 是 Throwable 的子类),但通常不应该。OutOfMemoryError 在某些特定场景下可以捕获(如尝试分配大数组失败后降级处理)。
Q4:throw 和 throws 的区别?
| throw | throws | |
|---|---|---|
| 位置 | 方法体内 | 方法签名上 |
| 作用 | 抛出一个异常对象 | 声明方法可能抛出的异常类型 |
| 数量 | 一次只能抛一个 | 可以声明多个 |
Q5:异常匹配是 instanceof 还是精确匹配?
是 instanceof 匹配。catch (Exception e) 能捕获所有 Exception 的子类。
# 12.10 总结与核心要点
异常处理的设计哲学:
- 分层设计:Error(系统级)→ 受检异常(可恢复)→ 运行时异常(程序 bug),三层分类对应不同的处理策略
- 异常表机制:try 块无开销,异常发生时才查表处理——"不出错不付费"
- 栈展开:异常沿调用栈逐层查找 handler,未找到则线程终止
- 代码复制:finally 通过复制代码到每个退出路径来保证执行
- Suppressed 异常:try-with-resources 解决了异常覆盖问题
核心要点速查:
| 问题 | 答案 |
|---|---|
| try-catch 有性能开销吗 | 不抛异常时几乎无开销,抛异常时开销在创建异常和栈展开 |
| 异常创建为什么慢 | fillInStackTrace() 需要遍历整个调用栈 |
| finally 怎么保证执行 | 编译器将 finally 代码复制到每个退出路径 |
| finally 中 return 的问题 | 会覆盖 try/catch 的返回值 |
| 受检异常好不好 | 有争议,现代趋势偏向非受检异常 |
| try-with-resources 原理 | 编译器生成 close() 调用,异常作为 suppressed 处理 |
| OmitStackTraceInFastThrow | JIT 优化,高频异常省略栈轨迹 |
| 异常转译 | 底层异常转为上层语义异常,保留 cause |
理解 JVM 的异常处理机制,有助于写出更健壮、高效的错误处理代码,避免异常滥用带来的性能问题。