跳至主要內容

Mr.Hope大约 5 分钟

一、异常体系的「基因密码」

1.1 Throwable家族的双螺旋结构

在JVM眼中,所有异常都是Throwable的子孙。Error是系统级的"绝症",比如OutOfMemoryError发生时,JVM的堆内存就像被撑爆的气球,连对象头都塞不下新对象了。这类异常的特点是:无法通过代码挽救,只能调整JVM参数或修复程序逻辑[1]open in new window[8]open in new window

Exception则是程序员能处理的"慢性病"。比如NullPointerException发生时,引用指针在栈帧中指向了无效的堆内存地址。这类异常的内存结构特点决定了它们可被捕获处理:

// 典型空指针场景
User user = null;
System.out.println(user.getName());  // 栈中的user引用指向null,触发NPE

1.2 Checked与Unchecked的哲学之争

Checked异常(如IOException)像编译器给你的TODO清单——必须显式处理才能通过编译。这种设计强制开发者考虑异常场景,但过度使用会导致代码臃肿:

// 经典检查型异常处理
try {
    Files.readAllBytes(Paths.get("config.ini"));
} catch (IOException e) {  // 必须捕获或声明throws
    System.out.println("配置文件读取失败");
}

Unchecked异常(如IllegalArgumentException)则是信任开发者的产物。编译器不做强制检查,但运行时一旦触发就会导致程序崩溃,适合表示编程错误[4]open in new window[12]open in new window

1.3 自定义异常的黄金法则

  • 业务异常继承Exception:比如支付超时异常需要调用方处理
  • 框架异常继承RuntimeException:如Spring的DataAccessException
  • 链式异常的正确姿势:
try {
    // 业务代码
} catch (SQLException e) {
    throw new ServiceException("数据库操作失败").initCause(e);  // 保留原始堆栈
}

二、异常处理的「底层密码」

2.1 try-catch-finally的字节码真相

JVM通过异常表(Exception Table)实现异常处理。每个try块对应一个异常表条目,包含监控范围、异常类型和处理地址。finallyjsr/ret指令在字节码层面实现了代码复用:

异常表:
起始指令  结束指令  处理程序地址  异常类型
0        4        8            java/io/IOException

字节码:
0: getstatic     #2  // 开始try块
3: pop
4: goto          20  // 正常执行跳转
7: astore_1         // 异常处理入口
8: jsr           15  // 跳转到finally块
11: aload_1
12: athrow          // 抛出异常
15: astore_2        // finally块开始
...
20: return          // 正常返回

2.2 资源关闭的「血泪史」

传统try-finally的陷阱:

InputStream is = null;
try {
    is = new FileInputStream("data");
    // 业务代码
} finally {
    if (is != null) is.close();  // close可能再次抛异常!
}

Try-with-resources的魔法:

try (InputStream is = new FileInputStream("data")) {  // 自动调用close()
    // 业务代码
}

背后的AutoCloseable接口通过编译器生成合成方法实现自动关闭,字节码层面比手动关闭更高效[43]open in new window

2.3 异常吞没的「替身攻击」

多资源关闭时使用addSuppressed()保留原始异常:

try {
    OutputStream os1 = new FileOutputStream("1.txt");
    OutputStream os2 = new FileOutputStream("2.txt");
} catch (IOException e) {
    Throwable t = new ResourceCloseException("资源关闭失败");
    t.addSuppressed(e);  // 保留原始异常
    throw t;
}

三、异常处理的「性能暗礁」

3.1 异常构造的代价

JMH测试显示,创建异常对象的开销是普通对象的100倍以上!因为需要收集完整的堆栈跟踪:

@Benchmark
public Exception createException() {
    return new Exception("test");
}

@Benchmark
public Exception createPrebuiltException() {
    return PREBUILT_EXCEPTION;  // 预创建异常实例
}

优化技巧:在高频代码路径中避免抛出异常,或复用异常实例(需确保线程安全)。

3.2 全局异常处理的「指挥链」

Spring的@ControllerAdvice基于责任链模式实现统一异常处理:

客户端请求 -> DispatcherServlet -> 控制器方法 -> 异常发生
                    ↑
                    └── @ControllerAdvice捕获异常并处理

Servlet容器的异常传递路径:

HTTP请求 -> Filter链 -> Servlet.service() -> 业务代码
                                    ↑
                                    └── web.xml配置的<error-page>

四、框架中的「异常江湖」

4.1 线程池的「沉默杀手」

  • execute():未捕获的异常会触发线程的UncaughtExceptionHandler
  • submit():异常被封装在Future中,只有调用get()时才会抛出
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(() -> { throw new RuntimeException(); });  // 异常被吞没
pool.execute(() -> { throw new RuntimeException(); }); // 触发UncaughtExceptionHandler

4.2 分布式系统的「烽火台」

Dubbo的异常传播机制:

消费者 -> 代理对象 -> 网络传输 -> 提供者
   ↑                        │
   └── RpcException ←───────┘ (序列化异常、超时异常等)

RPC框架通过异常码映射实现跨语言异常传递,例如Dubbo的RpcException封装了错误码和原始异常信息[29]open in new window

五、面试官的「灵魂拷问」

5.1 final、finally、finalize的「三胞胎之谜」

  • final:修饰类不可继承,方法不可重写,变量不可修改
  • finally:异常处理中的清理代码块
  • finalize:对象被GC前的最后机会(可能导致内存泄漏)

5.2 Error的「死亡证明」

StackOverflowError不可恢复的本质原因:JVM的线程栈空间耗尽,无法创建新的栈帧。可通过-XX:ThreadStackSize=256k调整栈大小,但治标不治本[5]open in new window[14]open in new window


特别放送

JDK 17+新特性

// Pattern Matching简化异常处理
try {
    // 可能抛出多种异常
} catch (IOException | SQLException e) {
    if (e instanceof IOException ioe) {  // 自动转型
        handleIOE(ioe);
    } else {
        handleSQLE((SQLException)e);
    }
}

Sealed Classes约束异常继承:

public sealed class BusinessException 
    permits PaymentException, OrderException {}  // 限制异常子类

反模式警示录

Lombok的@SneakyThrows绕过检查型异常:

public void readConfig() {
    @SneakyThrows
    String content = Files.readString(Path.of("config.ini"));  // 编译期不报错
}

延伸阅读

  • 《Effective Java》第69条:优先使用标准异常
  • JEP 390:废弃基于finalization的API
  • Java Flight Recorder分析异常热点

异常处理的艺术,在于平衡安全性与性能,理解规范更需看透本质。掌握这些原理和技巧,定能在面试和实战中游刃有余。