一、JVM 层面的异常实现(字节码与异常表)
- 异常表(Exception Table)
每个方法的字节码中都有一张异常表,结构如下:
```
起始PC | 结束PC | 处理PC | catch类型(类全限定名)
```
· try 块对应 [起始PC, 结束PC) 的指令范围
· 当该范围内抛出异常,JVM 匹配异常表:
· 若 catch 类型匹配,则跳转到 处理PC
· 若没有匹配项,则方法栈弹出,继续到调用者方法中查找
- finally 的字节码实现
· 编译器为 finally 块生成冗余代码:在 try 的正常出口、每个 catch 的出口,以及异常抛出的路径前,都会插入 finally 块的字节码拷贝。
· 因此如果 finally 中有 return,它会覆盖 try/catch 的返回或异常。
- athrow 指令
· 抛出异常时,JVM 执行 athrow 指令,将当前栈顶的异常引用抛出,并开始查找异常表。
二、try-with-resources 彻底解密
- 语法糖展开前 vs 展开后
原代码:
```java
try (FileInputStream fis = new FileInputStream("a.txt")) {
fis.read();
}
```
编译后等价于:
```java
FileInputStream fis = new FileInputStream("a.txt");
Throwable primaryExc = null;
try {
fis.read();
} catch (Throwable t) {
primaryExc = t;
throw t;
} finally {
if (fis != null) {
if (primaryExc != null) {
try {
fis.close();
} catch (Throwable suppressed) {
primaryExc.addSuppressed(suppressed);
}
} else {
fis.close();
}
}
}
```
· 自动管理多个资源(按声明顺序逆序关闭)
· 抑制异常:primaryExc 为主异常,close 中的异常被添加到抑制链
- 自定义自动关闭资源
实现 AutoCloseable 接口即可,close() 方法可以被调用多次(但幂等性由开发者保证)。
三、异常链与堆栈信息高级操作
- 保留原始异常的三种方式
```java
// 方式1:构造函数传 cause
throw new MyException("msg", originalException);
// 方式2:initCause
MyException e = new MyException("msg");
e.initCause(originalException);
// 方式3:throwable 的 fillInStackTrace 可覆盖堆栈
e.setStackTrace(new StackTraceElement[0]); // 抹去堆栈(不推荐)
```
- 堆栈轨迹的获取与过滤
· Thread.currentThread().getStackTrace() 可获取当前线程的调用栈(不包含异常)
· Throwable.getStackTrace() 返回异常填充时的快照
· Java 9+ 提供了 StackWalker 进行高效的栈遍历和过滤
```java
StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames.limit(5).collect(Collectors.toList()));
```
四、多线程与异步异常处理深度解析
- 不同提交方式的异常传播
方法 异常去向
Thread.start() 未捕获 线程消亡,异常传递给 Thread.UncaughtExceptionHandler(若设置)
Executor.execute(Runnable) 异常交给 Thread 的 UncaughtExceptionHandler
Executor.submit(Runnable) 异常被包装到返回的 Future<?>,future.get() 抛出 ExecutionException
Executor.submit(Callable) 同上,且 Callable 可以声明受检异常
- 全局未捕获异常处理器
```java
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) ->
logger.error("Thread {} failed", thread.getName(), throwable)
);
```
- 线程池内部异常处理最佳实践
```java
ThreadPoolExecutor pool = new ThreadPoolExecutor(1,1,0,TimeUnit.SECONDS, new LinkedBlockingQueue<>());
pool.setThreadFactory(r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, ex) -> {
// 记录异常,甚至重新抛出一个运行时异常
});
return t;
});
```
五、性能分析与调优(精确数据)
- 异常对象创建成本
· 填充堆栈轨迹是主要开销:遍历当前栈帧,获取类名、方法名、行号
· 一个异常对象耗时约 1~15 微秒(取决于深度),比普通 if 判断(纳秒级)高几个数量级
· 高频调用中(如每秒百万次),异常可导致吞吐量下降 50%+
- 减少异常开销的手段
· 使用 Throwable(String, Throwable, boolean, boolean) 构造器控制是否填充堆栈(Java 7+)
· 参数 writableStackTrace=false 生成无堆栈异常,性能接近普通对象
· 适用场景:已知异常类型但不需要堆栈信息(例如快速失败重试)
· 复用异常对象(不推荐,因为堆栈无法正确反映调用点,且并发时线程不安全)
```java
// 无堆栈异常示例
public class FastException extends Exception {
public FastException() {
super("msg", null, false, false);
}
}
```
- 实测对比(粗略参考)
操作 耗时(ns)
if 判断 1~2
new Object() 10~20
new Exception() 1000~5000
new Exception(true) 100~200
六、框架及架构层面专项设计
- 统一异常处理(Spring Boot)
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handle(IllegalArgumentException e) {
return new ErrorResponse(e.getMessage());
}
}
```
· 注意多个 @ExceptionHandler 的顺序:子类优先于父类
· 可以使用 @ControllerAdvice(assignableTypes = {...}) 限定范围
- 分布式系统中的异常封装
· 网络调用异常应明确区分可重试(服务超时、连接重置)和不可重试(业务参数错误)
· 返回前端的错误码与异常类型应隔离:不要将 SQLSyntaxErrorException 直接暴露给用户
```java
// 典型模式
try {
remoteCall();
} catch (TimeoutException e) {
throw new RetryableException("remote timeout");
} catch (RemoteException e) {
throw new BizException("remote error", e);
}
```
- 异步回调中的异常处理(CompletableFuture)
· CompletableFuture 提供了 exceptionally、handle 等方法:
```java
CompletableFuture.supplyAsync(() -> {
if (error) throw new RuntimeException("oops");
return "ok";
}).exceptionally(ex -> "fallback")
.thenAccept(System.out::println);
```
· 不处理异常会导致 future.join() / get() 抛出 CompletionException
七、复杂场景陷阱与源码验证
- finally 块与 return 的交互详细测试
情况 结果
try { return 1; } finally { return 2; } 返回 2,try 中的返回值被抛弃
try { throw new Ex(); } finally { return; } 异常被吞掉,方法正常返回
try { return obj; } finally { obj = null; } 返回原 obj 的引用,obj 重新赋值不影响返回值(前提是引用变量)
- 捕获 Error 是否合理
· 理论上 Error 表示 JVM 严重问题(如 OutOfMemoryError),通常不应捕获
· 但在资源清理代码中,有时需要捕获 Throwable 确保 finally 执行(如框架底层)
- 为什么重写 finalize() 时建议捕获 Throwable?
· finalize() 中任何异常会被忽略,对象被标记为已终结;因此建议捕获所有异常并进行日志。
八、专项自测题(带答案)
- try-with-resources 中,如果资源构造器抛异常,资源会关闭吗?
答:不会。资源变量为 null,所以不会调用 close。
- 一个方法同时声明 throws IOException, SQLException,catch 块可以写 catch (Exception e) 吗?
答:可以,但不推荐,会捕获所有异常,包括未声明的 RuntimeException。
- 什么是"抑制异常"?请给出一个典型场景
答:try-with-resources 中,主异常抛出后,close() 又抛出异常,后者被附加为主异常的抑制异常。
4.Thread.setDefaultUncaughtExceptionHandler 对 execute 和 submit 都生效吗?
答:仅对 execute 且未设置线程单独 Handler 时生效;submit 返回的 Future 吞掉了异常(转为返回值)。