编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
        • 12.1 开篇疑问
        • 12.2 异常体系全景
          • 12.2.1 Throwable家族树
          • 12.2.2 Error与Exception的设计区分
          • 12.2.3 受检异常与非受检异常
          • 12.2.4 常见异常分类速查
        • 12.3 JVM异常处理机制
          • 12.3.1 异常表的字节码结构
          • 12.3.2 多重catch的异常表
          • 12.3.3 异常匹配与栈展开
          • 12.3.4 finally的实现原理
          • 12.3.5 finally的经典陷阱
        • 12.4 异常的性能代价
          • 12.4.1 创建异常的开销
          • 12.4.2 fillInStackTrace源码分析
          • 12.4.3 异常与正常控制流对比
          • 12.4.4 JIT对异常的优化
        • 12.5 异常设计的哲学争论
          • 12.5.1 受检异常之争
          • 12.5.2 各语言的异常设计对比
          • 12.5.3 异常的正确使用原则
        • 12.6 try-with-resources原理
          • 12.6.1 传统资源释放的问题
          • 12.6.2 try-with-resources编译器转换
          • 12.6.3 Suppressed异常机制
        • 12.7 异常处理最佳实践
          • 12.7.1 异常转译与异常链
          • 12.7.2 全局异常处理
          • 12.7.3 自定义异常体系设计
          • 12.7.4 日志与异常的配合
        • 12.8 JDK版本中异常的演进
        • 12.9 常见面试深度问题
        • 12.10 总结与核心要点
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

异常体系与JVM机制

# 04.异常体系与JVM机制

# 目录介绍

  • 12.1 开篇疑问
  • 12.2 异常体系全景
    • 12.2.1 Throwable家族树
    • 12.2.2 Error与Exception的设计区分
    • 12.2.3 受检异常与非受检异常
    • 12.2.4 常见异常分类速查
  • 12.3 JVM异常处理机制
    • 12.3.1 异常表的字节码结构
    • 12.3.2 多重catch的异常表
    • 12.3.3 异常匹配与栈展开
    • 12.3.4 finally的实现原理
    • 12.3.5 finally的经典陷阱
  • 12.4 异常的性能代价
    • 12.4.1 创建异常的开销
    • 12.4.2 fillInStackTrace源码分析
    • 12.4.3 异常与正常控制流对比
    • 12.4.4 JIT对异常的优化
  • 12.5 异常设计的哲学争论
    • 12.5.1 受检异常之争
    • 12.5.2 各语言的异常设计对比
    • 12.5.3 异常的正确使用原则
  • 12.6 try-with-resources原理
    • 12.6.1 传统资源释放的问题
    • 12.6.2 try-with-resources编译器转换
    • 12.6.3 Suppressed异常机制
  • 12.7 异常处理最佳实践
    • 12.7.1 异常转译与异常链
    • 12.7.2 全局异常处理
    • 12.7.3 自定义异常体系设计
    • 12.7.4 日志与异常的配合
  • 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
1
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);
    // 释放一些缓存,降级处理
}
1
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
    }
}
1
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();
    }
}
1
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
1
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);
}
1
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
1
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
1
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 处理
   → 默认行为:打印异常堆栈,线程终止
1
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 块
1
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();
}
1
2
3
4
5
6
7

编译后等价于:

try 正常路径:
  执行 try 块
  复制一份 finally 代码(cleanup())
  return 1

catch 路径:
  执行 catch 块
  复制一份 finally 代码(cleanup())
  return 2

异常未捕获路径:
  复制一份 finally 代码(cleanup())
  重新抛出异常(athrow)
1
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中又抛异常时)
1
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 被丢弃
1
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 的修改对调用者可见)
1
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 异常)
1
2
3
4
5
6
7
8
9
10

# 12.4 异常的性能代价

# 12.4.1 创建异常的开销

创建异常对象的主要开销在于填充栈轨迹(fillInStackTrace()):

// 创建异常时,JVM 需要遍历整个调用栈,记录每一层的类名、方法名、行号
// 调用栈越深,开销越大
Throwable t = new Exception("test");
// 内部调用: fillInStackTrace() → 遍历几十甚至上百层栈帧
1
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;
    }
}
1
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]);  // 清空栈轨迹
}
1
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 倍)
1
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
1
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,一个底层异常影响整个调用链
1
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);  // 包装为非受检异常
        }
    });
1
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 异常的正确使用原则

  1. 异常用于异常情况,不用于正常控制流
  2. 优先使用标准异常:IllegalArgumentException、IllegalStateException、NullPointerException 等
  3. 提供有意义的异常消息:包含出错的参数值、期望条件等
  4. 不要忽略异常:空 catch 块是最危险的代码
  5. 异常转译而非暴露底层实现:上层不该知道底层用的是 SQL 还是文件
  6. 尽早失败(Fail Fast):在方法入口检查参数,不要等到深处才抛异常
// 反面教材:空 catch 块
try {
    riskyOperation();
} catch (Exception e) {
    // 什么都不做——最危险的代码!
    // 异常被吞掉,问题悄无声息地发生
}

// 正确做法
try {
    riskyOperation();
} catch (Exception e) {
    log.error("操作失败: " + context, e);  // 至少记录日志
    throw new ServiceException("操作失败", e);  // 或转译后重新抛出
}
1
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 的异常?还是覆盖原异常?
            // 两种做法都有问题
        }
    }
}
1
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 自动关闭
1
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();  // 正常关闭
        }
    }
}
1
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
1
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
1
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()); // 可能还有更底层的原因
}
1
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;
    });
1
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;
    // ...
}
1
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);  // 只抛出,让上层统一记录
}
1
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
1
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) {  // 直接使用已声明的变量
    // ...
}
1
2
3
4
5
6
7
8
9
10
11

# 12.9 常见面试深度问题

Q1:try-catch 有性能开销吗?

try 块本身几乎无开销(只是异常表中增加一条记录)。开销在于:异常对象的创建(fillInStackTrace 遍历调用栈)和异常的匹配(栈展开遍历调用链)。

Q2:finally 一定会执行吗?

几乎一定,除了以下情况:

  1. System.exit() 被调用
  2. JVM 崩溃(如 JNI 错误导致 JVM 进程终止)
  3. 线程被 Thread.stop()(已废弃)
  4. 所在线程是守护线程,JVM 正常退出时

Q3:Error 可以被 catch 吗?

语法上可以(Error 是 Throwable 的子类),但通常不应该。OutOfMemoryError 在某些特定场景下可以捕获(如尝试分配大数组失败后降级处理)。

Q4:throw 和 throws 的区别?

throw throws
位置 方法体内 方法签名上
作用 抛出一个异常对象 声明方法可能抛出的异常类型
数量 一次只能抛一个 可以声明多个

Q5:异常匹配是 instanceof 还是精确匹配?

是 instanceof 匹配。catch (Exception e) 能捕获所有 Exception 的子类。

# 12.10 总结与核心要点

异常处理的设计哲学:

  1. 分层设计:Error(系统级)→ 受检异常(可恢复)→ 运行时异常(程序 bug),三层分类对应不同的处理策略
  2. 异常表机制:try 块无开销,异常发生时才查表处理——"不出错不付费"
  3. 栈展开:异常沿调用栈逐层查找 handler,未找到则线程终止
  4. 代码复制:finally 通过复制代码到每个退出路径来保证执行
  5. Suppressed 异常:try-with-resources 解决了异常覆盖问题

核心要点速查:

问题 答案
try-catch 有性能开销吗 不抛异常时几乎无开销,抛异常时开销在创建异常和栈展开
异常创建为什么慢 fillInStackTrace() 需要遍历整个调用栈
finally 怎么保证执行 编译器将 finally 代码复制到每个退出路径
finally 中 return 的问题 会覆盖 try/catch 的返回值
受检异常好不好 有争议,现代趋势偏向非受检异常
try-with-resources 原理 编译器生成 close() 调用,异常作为 suppressed 处理
OmitStackTraceInFastThrow JIT 优化,高频异常省略栈轨迹
异常转译 底层异常转为上层语义异常,保留 cause

理解 JVM 的异常处理机制,有助于写出更健壮、高效的错误处理代码,避免异常滥用带来的性能问题。

上次更新: 2026/06/10, 11:13:41
垃圾回收与GC调优
字节码指令集javap实战

← 垃圾回收与GC调优 字节码指令集javap实战→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式