异常
异常的层次结构
-
异常定义: 程序运行期间发生的不期而至的状况,干扰正常指令流程的事件。Java 异常都是 Throwable 类的子类实例,描述错误条件。
-
Throwable 类: Java 错误与异常的超类,包含两个子类:Error (错误) 和 Exception (异常)。提供
printStackTrace()
等接口获取堆栈跟踪信息。-
Error (错误):
- 程序无法处理的严重错误,通常表示 JVM 自身出现问题。
- 例如
OutOfMemoryError
(内存不足),StackOverflowError
(栈溢出) 等。 - 不受检异常,非代码性错误,应用程序不应处理。
-
JVM 通常会终止线程。
- Exception (异常):
-
程序可以捕获和处理的异常。
- 分为两类:
- 运行时异常 (RuntimeException):
RuntimeException
类及其子类,例如NullPointerException
,IndexOutOfBoundsException
。- 不检查异常,可以选择捕获处理,也可以不处理。
- 通常由程序逻辑错误引起,应尽可能避免。
- 编译器不强制检查。
- 编译时异常 (非运行时异常/可查异常):
RuntimeException
以外的Exception
类及其子类,例如IOException
,SQLException
。- 必须处理的异常,不处理则编译不通过。
- 编译器强制检查,必须
try-catch
捕获或throws
声明抛出。
- 运行时异常 (RuntimeException):
- 可查异常 vs. 不可查异常:
-
可查异常 (Checked Exceptions):
- 编译器要求必须处理 的异常。
- 程序运行中容易出现、可预测的异常状况。
- 例如,文件找不到 (
FileNotFoundException
)、网络连接失败 (IOException
)。 - 必须
try-catch
捕获或throws
声明抛出。 -
除了
RuntimeException
及其子类,其他Exception
子类都是可查异常。- 不可查异常 (Unchecked Exceptions):
-
编译器不强制处理 的异常。
- 包括 运行时异常 (RuntimeException 及其子类) 和 错误 (Error)。
-
异常基础
-
异常关键字:
try
: 监听代码块,可能抛出异常的代码放入try
块。catch
: 捕获try
块中发生的异常。finally
: 总是被执行 的代码块,用于资源回收 (如数据库连接、文件流)。finally
执行完后,才会执行try
或catch
块中的return
或throw
语句,除非finally
中也使用了return
或throw
等终止方法语句。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
包括Error
和Exception
,不应捕获Error
,除非确定能处理严重错误。 - 不要忽略异常:
catch
块至少要记录异常信息 (日志)。 - 不要记录并抛出异常: 避免 重复日志,除非需要 包装异常 为自定义异常并添加更具体信息。
- 包装异常时保留原始异常: 使用
Exception
接受Throwable
参数的构造函数,保留原始异常堆栈信息,方便问题分析。 - 不要使用异常控制程序流程: 异常用于异常情况,条件判断用
if
语句,避免性能问题。 - 不在
finally
块中使用return
:finally
中的return
会 覆盖try
或catch
块的return
值。
- 只在异常情况下使用异常: 异常用于不正常条件,不用于正常控制流程。避免
深入理解异常
-
JVM 异常处理机制:
- 异常表 (Exception Table): Class 文件中存储异常处理信息。
-
异常处理流程:
- 异常发生时,JVM 在当前方法的异常表中查找匹配的处理者 (from, to, type)。
- 找到匹配处理者,调用
target
位置的处理者 (catch 块)。 - 未找到匹配,向上查找调用方法的异常表 (弹栈)。
- 所有栈帧弹出仍未处理,抛给 当前线程 (Thread)。
- 线程终止,若为最后非守护线程,JVM 终止运行。
-
try-catch-finally
原理:finally
块代码被 复制到try
和catch
块的出口处 (字节码层面)。 catch
顺序: 具体异常先catch
,宽泛异常后catch
,否则编译错误。return
和finally
:finally
块在return
前执行,但finally
中的return
会覆盖try
或catch
的return
值。-
异常耗时:
-
异常创建耗时: 创建异常对象比普通对象更耗时 (约 20 倍)。
- 异常抛出和捕获耗时: 抛出和捕获异常更耗时 (约为创建异常对象的 4 倍)。
- 主要耗时点: 堆栈跟踪 (Stack Trace) 的生成。 异常性能分析:异常慢在哪里?
-
异常处理的主要性能开销并非在于异常对象的创建,而是在于抛出和捕获异常的过程。
异常抛出时 JVM 的行为
- 字节码分析 (
javap -verbose
): 分析catchException()
方法的字节码,重点关注athrow
指令。 athrow
指令的运作过程:- 检查栈顶异常对象: 确认类型为
java.lang.Throwable
的子类。 - 异常对象出栈: 暂时将异常对象引用从操作栈中移除。
- 搜索异常表: 在当前方法的异常表中查找合适的异常处理器 (handler)。
- 找到 Handler:
- 重置 PC 寄存器: 指向异常 handler 的起始指令。
- 清空操作栈: 为异常处理做准备。
- 异常对象入栈: 将之前出栈的异常对象引用重新压入操作栈,供
catch
块访问。 - 跳转到 Handler: 程序执行流程跳转到异常 handler 的代码。
- 未找到 Handler:
- 栈帧出栈: 将当前方法的栈帧从 JVM 栈中弹出。
- 向上查找: 在调用方法的栈帧中重复异常处理查找过程。
- 线程终止 (最终): 若所有栈帧都弹出仍未找到 handler,线程被迫终止。
- 检查栈顶异常对象: 确认类型为
- 异常表 (Exception Table): 记录了异常处理信息,包括异常作用范围 (
from
-to
)、异常类型 (type
)、异常处理器位置 (target
)。catch
和finally
关键字的实现都依赖于字节码和异常表。 - 总结
athrow
指令的 JVM 行为: 涉及类型检查、异常表搜索、程序计数器重置、栈操作、栈帧操作,甚至线程终止等复杂步骤,这些操作共同导致了异常处理的性能开销。
HotSpot VM 源码分析
- 源码位置: OpenJDK HotSpot VM 源码
bytecodeInterpreter.cpp
中的CASE(_athrow)
部分。 - 关键步骤:
- 提取异常对象: 从操作栈中获取异常对象引用 (
STACK_OBJECT(-1)
). - 空指针检查:
CHECK_NULL(except_oop)
,如果异常对象为空,则抛出NullPointerException
。 - 设置
pending_exception
:THREAD->set_pending_exception(except_oop, NULL, 0)
,将异常对象设置为线程的待处理异常。 - 跳转到
handle_exception
:goto handle_exception
,进入通用的异常处理流程。
- 提取异常对象: 从操作栈中获取异常对象引用 (
handle_exception
关键代码:- 查找异常处理器:
CALL_VM(continuation_bci = (intptr_t)InterpreterRuntime::exception_handler_for_exception(THREAD, except_oop()), handle_exception)
,通过InterpreterRuntime::exception_handler_for_exception
查找异常表。 - 找到 Handler (
continuation_bci >= 0
):- 异常对象入栈:
SET_STACK_OBJECT(except_oop(), 0)
。 - 重置 PC 指针:
pc = METHOD->code_base() + continuation_bci
,指向 handler 代码。 - 跳转到
run
:goto run
,继续执行。
- 异常对象入栈:
- 未找到 Handler:
- 重新设置
pending_exception
:THREAD->set_pending_exception(except_oop(), NULL, 0)
。 - 跳转到
handle_return
:goto handle_return
,进行方法退出处理。
- 重新设置
- 查找异常处理器:
- 结论: HotSpot VM 的
athrow
指令实现印证了字节码分析的结论,异常处理涉及复杂的查找和跳转过程,导致性能开销。