异常

异常的层次结构

  • 异常定义: 程序运行期间发生的不期而至的状况,干扰正常指令流程的事件。Java 异常都是 Throwable 类的子类实例,描述错误条件。

  • Throwable 类: Java 错误与异常的超类,包含两个子类:Error (错误)Exception (异常)。提供 printStackTrace() 等接口获取堆栈跟踪信息

    • Error (错误):

      • 程序无法处理的严重错误,通常表示 JVM 自身出现问题
      • 例如 OutOfMemoryError (内存不足), StackOverflowError (栈溢出) 等。
      • 不受检异常非代码性错误应用程序不应处理
      • JVM 通常会终止线程

        • Exception (异常):
      • 程序可以捕获和处理的异常

      • 分为两类:
        • 运行时异常 (RuntimeException):
          • RuntimeException 类及其子类,例如 NullPointerException, IndexOutOfBoundsException
          • 不检查异常可以选择捕获处理,也可以不处理
          • 通常由程序逻辑错误引起,应尽可能避免。
          • 编译器不强制检查
        • 编译时异常 (非运行时异常/可查异常):
          • RuntimeException 以外的 Exception 类及其子类,例如 IOException, SQLException
          • 必须处理的异常不处理则编译不通过
          • 编译器强制检查,必须 try-catch 捕获或 throws 声明抛出。
      • 可查异常 vs. 不可查异常:
    • 可查异常 (Checked Exceptions):

      • 编译器要求必须处理 的异常。
      • 程序运行中容易出现、可预测的异常状况。
      • 例如,文件找不到 (FileNotFoundException)、网络连接失败 (IOException)。
      • 必须 try-catch 捕获或 throws 声明抛出
      • 除了 RuntimeException 及其子类,其他 Exception 子类都是可查异常。

        • 不可查异常 (Unchecked Exceptions):
      • 编译器不强制处理 的异常。

      • 包括 运行时异常 (RuntimeException 及其子类)错误 (Error)

异常基础

  • 异常关键字:

    • try: 监听代码块,可能抛出异常的代码放入 try 块。
    • catch: 捕获 try 块中发生的异常。
    • finally: 总是被执行 的代码块,用于资源回收 (如数据库连接、文件流)。finally 执行完后,才会执行 trycatch 块中的 returnthrow 语句,除非 finally 中也使用了 returnthrow 等终止方法语句。
    • throw: 手动抛出 异常。
    • throws: 在方法签名中声明方法可能抛出的异常
    • 异常声明 (throws):

    • 方法中若存在编译时异常,且不捕获,必须在方法签名中使用 throws 声明,告知调用者处理异常。

    • 声明多个异常用逗号分隔
    • 子类方法 覆盖父类方法时,不能声明新的或更宽泛的异常,声明的异常必须是父类方法声明异常的同类或子类,或者不声明异常 (如果父类方法没有声明异常)。
    • 规则:
      • 不可查异常 (Error, RuntimeException):可以不声明,编译通过,运行时可能抛出。
      • 可查异常 (Checked Exception):必须声明或捕获,否则编译错误。
      • 仅当方法 抛出异常 时,调用者才需要处理或声明抛出。
    • 异常抛出 (throw):

    • 使用 throw 关键字 手动抛出异常实例

    • 通常用于 指示代码错误或异常情况
    • 多系统集成时,可用于 转换异常类型,统一对外暴露异常,隐藏内部细节。
    • 自定义异常:

    • 自定义异常类通常继承 Exception (编译时异常) 或 RuntimeException 类 (运行时异常)。

    • 习惯包含两个构造函数:
      • 无参构造函数 (public MyException(){ })
      • 带详细描述信息构造函数 (public MyException(String msg){ super(msg); }),方便调试和日志记录。
    • 异常捕获:

    • try-catch: 捕获并处理异常。

      • 可以 捕获多种异常类型不同 catch 块处理不同异常
      • 同一个 catch 也可以 捕获多种异常,用 | 分隔
    • try-catch-finally: 完整异常处理结构,finally确保资源回收
      • 执行顺序:
        • try 无异常: try -> finally -> 后续代码。
        • try 有异常,catch 无匹配: try (异常点前代码) -> finally -> 异常抛给 JVM,后续代码不执行。
        • try 有异常,catch 有匹配: try (异常点前代码) -> catch (匹配的块) -> finally -> 后续代码。
    • try-finally: 不捕获异常,但保证 finally 块执行,用于资源清理。
      • try 块异常,异常代码后语句不执行,直接执行 finally
      • try 块无异常,执行完 try 后执行 finally
      • 例如,IO 流关闭、Lock 释放等。
      • finally 不执行的情况: System.exit(), finally 块内异常, 线程死亡, CPU 关闭。
    • try-with-resource (Java 7+): 自动资源管理,资源需实现 AutoCloseable 接口。
      • try() 中声明资源,try 块结束后自动调用 close() 关闭资源
      • close() 抛出异常,会被抑制,原始异常优先抛出。被抑制的异常可通过 getSuppressed() 获取。
    • 异常基础总结:

    • try, catch, finally 不能单独使用,必须组合 (try-catch, try-finally, try-catch-finally)。

    • try 监控代码,异常发生则停止 try 块后续代码,交给 catch 处理。
    • finally 代码块一定执行,用于资源回收。
    • throws 声明异常,throw 抛出异常。
    • Java 编程思想异常总结:恰当级别处理问题、解决问题重试、修补绕过、替换数据、完成当前工作后抛出、转换异常抛出、终止程序、简化异常模式、安全类库和程序。

异常实践

  • 最佳实践:

    • 只在异常情况下使用异常: 异常用于不正常条件不用于正常控制流程。避免 catch 预检查可以规避的 RuntimeException (如 NullPointerException, IndexOutOfBoundsException)。
    • finally 清理资源或 try-with-resource: 确保资源 (如 IO 流) 在使用后被关闭
    • 尽量使用标准异常: 重用 Java 标准异常,API 更易用,代码更易读,减少异常类数量。例如 IllegalArgumentException, IllegalStateException, NullPointerException, IndexOutOfBoundsException, ConcurrentModificationException, UnsupportedOperationException
    • 对异常进行文档说明: 使用 Javadoc @throws 声明,描述异常场景,提供足够信息给调用者。
    • 优先捕获最具体的异常: catch 块顺序应 从具体到宽泛,避免捕获父类异常导致子类 catch 块无法执行。
    • 不要捕获 Throwable: Throwable 包括 ErrorException不应捕获 Error,除非确定能处理严重错误。
    • 不要忽略异常: catch至少要记录异常信息 (日志)。
    • 不要记录并抛出异常: 避免 重复日志,除非需要 包装异常 为自定义异常并添加更具体信息。
    • 包装异常时保留原始异常: 使用 Exception 接受 Throwable 参数的构造函数,保留原始异常堆栈信息,方便问题分析。
    • 不要使用异常控制程序流程: 异常用于异常情况条件判断用 if 语句,避免性能问题。
    • 不在 finally 块中使用 return: finally 中的 return覆盖 trycatch 块的 return

深入理解异常

  • JVM 异常处理机制:

    • 异常表 (Exception Table): Class 文件中存储异常处理信息。
    • 异常处理流程:

      1. 异常发生时,JVM 在当前方法的异常表中查找匹配的处理者 (from, to, type)。
      2. 找到匹配处理者,调用 target 位置的处理者 (catch 块)。
      3. 未找到匹配,向上查找调用方法的异常表 (弹栈)。
      4. 所有栈帧弹出仍未处理,抛给 当前线程 (Thread)
      5. 线程终止,若为最后非守护线程,JVM 终止运行。
    • try-catch-finally 原理: finally 块代码被 复制到 trycatch 块的出口处 (字节码层面)。

    • catch 顺序: 具体异常先 catch,宽泛异常后 catch,否则编译错误。
    • returnfinally: finally 块在 return 前执行,但 finally 中的 return 会覆盖 trycatchreturn 值。
    • 异常耗时:

    • 异常创建耗时: 创建异常对象比普通对象更耗时 (约 20 倍)。

    • 异常抛出和捕获耗时: 抛出和捕获异常更耗时 (约为创建异常对象的 4 倍)。
    • 主要耗时点: 堆栈跟踪 (Stack Trace) 的生成异常性能分析:异常慢在哪里?
  • 异常处理的主要性能开销并非在于异常对象的创建,而是在于抛出和捕获异常的过程。

异常抛出时 JVM 的行为

  • 字节码分析 (javap -verbose): 分析 catchException() 方法的字节码,重点关注 athrow 指令。
  • athrow 指令的运作过程:
    1. 检查栈顶异常对象: 确认类型为 java.lang.Throwable 的子类。
    2. 异常对象出栈: 暂时将异常对象引用从操作栈中移除。
    3. 搜索异常表: 在当前方法的异常表中查找合适的异常处理器 (handler)。
    4. 找到 Handler:
      • 重置 PC 寄存器: 指向异常 handler 的起始指令。
      • 清空操作栈: 为异常处理做准备。
      • 异常对象入栈: 将之前出栈的异常对象引用重新压入操作栈,供 catch 块访问。
      • 跳转到 Handler: 程序执行流程跳转到异常 handler 的代码。
    5. 未找到 Handler:
      • 栈帧出栈: 将当前方法的栈帧从 JVM 栈中弹出。
      • 向上查找: 在调用方法的栈帧中重复异常处理查找过程。
      • 线程终止 (最终): 若所有栈帧都弹出仍未找到 handler,线程被迫终止。
  • 异常表 (Exception Table): 记录了异常处理信息,包括异常作用范围 (from-to)、异常类型 (type)、异常处理器位置 (target)。catchfinally 关键字的实现都依赖于字节码和异常表。
  • 总结 athrow 指令的 JVM 行为: 涉及类型检查、异常表搜索、程序计数器重置、栈操作、栈帧操作,甚至线程终止等复杂步骤,这些操作共同导致了异常处理的性能开销。

HotSpot VM 源码分析

  • 源码位置: OpenJDK HotSpot VM 源码 bytecodeInterpreter.cpp 中的 CASE(_athrow) 部分。
  • 关键步骤:
    1. 提取异常对象: 从操作栈中获取异常对象引用 (STACK_OBJECT(-1)).
    2. 空指针检查: CHECK_NULL(except_oop),如果异常对象为空,则抛出 NullPointerException
    3. 设置 pending_exception: THREAD->set_pending_exception(except_oop, NULL, 0),将异常对象设置为线程的待处理异常。
    4. 跳转到 handle_exception: goto handle_exception,进入通用的异常处理流程。
  • handle_exception 关键代码:
    1. 查找异常处理器: CALL_VM(continuation_bci = (intptr_t)InterpreterRuntime::exception_handler_for_exception(THREAD, except_oop()), handle_exception),通过 InterpreterRuntime::exception_handler_for_exception 查找异常表。
    2. 找到 Handler (continuation_bci >= 0):
      • 异常对象入栈: SET_STACK_OBJECT(except_oop(), 0)
      • 重置 PC 指针: pc = METHOD->code_base() + continuation_bci,指向 handler 代码。
      • 跳转到 run: goto run,继续执行。
    3. 未找到 Handler:
      • 重新设置 pending_exception: THREAD->set_pending_exception(except_oop(), NULL, 0)
      • 跳转到 handle_return: goto handle_return,进行方法退出处理。
  • 结论: HotSpot VM 的 athrow 指令实现印证了字节码分析的结论,异常处理涉及复杂的查找和跳转过程,导致性能开销。