Throwable / Exception 异常体系源码
概述
java.lang.Throwable 是 Java 异常体系的基类。所有异常类型——包括 Exception、RuntimeException、Error——都直接或间接继承自 Throwable。Throwable 封装了异常发生时的完整上下文:错误消息、调用栈快照、异常链,以及 JDK 7 引入的抑制异常机制。
本文基于 OpenJDK 21 源码,从 Throwable 的核心属性出发,逐层深入各关键方法的设计原理与 JVM 层面的实现细节。
java.lang.Throwable
├── java.lang.Error (不可恢复的系统级错误)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── java.lang.Exception (可恢复的程序级异常)
├── RuntimeException (非检查型异常)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ └── ...
└── 检查型异常 (Checked Exception)
├── IOException
├── SQLException
└── ...本文基于 OpenJDK 21 源码分析,涉及 JDK 7 引入的
suppressedExceptions、JDK 14+ 的NullPointerException增强等版本差异。
1. Throwable 的 3 个核心属性
Throwable 的核心状态由三个关键字段构成,它们共同定义了异常对象的全部信息:
// java.lang.Throwable(核心字段)
public class Throwable implements Serializable {
// 1. 详细错误消息
private String detailMessage;
// 2. 根本原因(链式异常)
private Throwable cause = this; // 初始化为自身,表示"无原因"
// 3. 调用栈快照
private StackTraceElement[] stackTrace;
// JDK 7+:是否记录可写栈,false 时跳过 fillInStackTrace()
private boolean writableStackTrace;
// JDK 7+:抑制异常列表
private List<Throwable> suppressedExceptions;
}1.1 detailMessage — 详细错误消息
public String getMessage() {
return detailMessage;
}
public String getLocalizedMessage() {
return getMessage();
}detailMessage 是异常的描述性文本,通常由构造器传入。getMessage() 直接返回,getLocalizedMessage() 默认委托给 getMessage(),子类可重写以实现本地化。getLocalizedMessage() 被 toString() 内部调用。
1.2 cause — 根本原因(异常链)
cause 字段用于构建异常链——低层异常作为高层异常的"根本原因"。初始值为 this(JDK 源码中赋值 this 作为哨兵值,表示"尚未设置 cause")。
public synchronized Throwable initCause(Throwable cause) {
if (this.cause != this)
throw new IllegalStateException("Can't overwrite cause");
if (cause == this)
throw new IllegalArgumentException("Self-causation not permitted");
this.cause = cause;
return this;
}
public synchronized Throwable getCause() {
return (cause == this ? null : cause);
}initCause() 的设计允许在构造器之后设置 cause——对于不支持 cause 参数的早期构造器,这是一个向后兼容的方案。但每个异常只能设置一次 cause。
1.3 stackTrace — 调用栈快照
stackTrace 是一个 StackTraceElement[] 数组,由 fillInStackTrace() 填充,记录了异常抛出时线程的完整调用栈。
1.4 构造器链
// 无参构造器
public Throwable() {
fillInStackTrace(); // 填充调用栈
}
// 带消息
public Throwable(String message) {
fillInStackTrace();
detailMessage = message;
}
// 带消息和原因
public Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this.cause = cause;
}
// JDK 7+:控制是否填栈和是否支持异常链
protected Throwable(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
if (writableStackTrace) {
fillInStackTrace();
} else {
stackTrace = NO_STACK_TRACE; // 空数组,跳过填栈
}
detailMessage = message;
this.cause = cause;
if (!enableSuppression)
suppressedExceptions = SUPPRESSED_NO_EXCEPTIONS; // 空列表
}构造器链清晰地展示了 Throwable 的设计层次:消息 → 原因 → 是否记录栈 → 是否允许抑制。每个新增参数都在前一层上增加控制粒度。
2. Throwable.fillInStackTrace() 的 native 实现
public synchronized native Throwable fillInStackTrace();fillInStackTrace() 是一个 synchronized native 方法,是 Throwable 性能敏感的核心操作。
2.1 HotSpot 实现
// hotspot/share/prims/jvm.cpp
JVM_ENTRY(void, JVM_FillInStackTrace(JNIEnv *env, jobtype receiver)) {
// 获取当前线程
JavaThread* thread = JavaThread::current();
// 获取调用栈帧并进行快照
// 跳过 Throwable 自身的构造器和 fillInStackTrace 帧
int depth = 0;
for (vframeStream vfst(thread); !vfst.at_end(); vfst.next()) {
// vfst.method() 获取当前帧的方法信息
// 收集类名、方法名、文件名、行号
StackTraceElement* ste = new StackTraceElement(
vfst.method()->method_holder()->external_name(), // 类名
vfst.method()->name()->as_C_string(), // 方法名
vfst.method()->method_holder()->source_file(), // 文件名
vfst.method()->line_number_from_bci(vfst.bci()) // 行号
);
depth++;
}
// 分配 StackTraceElement[] 数组,写入对象
}2.2 执行流程
异常抛出的瞬间:
线程调用栈(栈顶在下):
┌─────────────────────────────────────┐
│ method_3() │ ← 抛出异常的位置
│ │ │
│ method_2() │ ← 中间调用
│ │ │
│ method_1() │ ← 最外层调用
└─────────────────────────────────────┘
│
▼
fillInStackTrace() 遍历栈帧(vframeStream)
┌─────────────────────────────────────┐
│ StackTraceElement[0]: method_3:42 │ ← 实际抛出位置
│ StackTraceElement[1]: method_2:28 │
│ StackTraceElement[2]: method_1:15 │
└─────────────────────────────────────┘fillInStackTrace() 会跳过 Throwable 构造器及其自身的栈帧,因此调用栈的顶层总是业务代码中抛出异常的位置,而非 Throwable 的内部构造。
2.3 性能开销与 writableStackTrace
JVM 遍历栈帧的过程需要:
- 遍历线程的 Java 栈
- 为每个栈帧查找运行时常量池中的方法信息
- 解析行号表(LineNumberTable)将字节码索引映射为源码行号
性能开销随栈深度线性增长。典型的 Web 框架调用栈可达 50~100 层,频繁创建异常但不需要栈信息时性能损耗显著。
JDK 7 引入的四参数构造器允许禁用栈记录:
protected Throwable(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {当 writableStackTrace = false 时,fillInStackTrace() 被跳过,stackTrace 被设为空数组 NO_STACK_TRACE。这是已知 java.lang.reflect.Proxy 中采用的优化策略——代理类产生的异常不需要调用栈信息。
// java.lang.reflect.Proxy 中的使用示例
// (通过反射调用时抛出的 InvocationTargetException 可在 jdk.internal.reflect 中看到此模式)
new InvocationTargetException(cause, null, false, false);
// writableStackTrace = false, enableSuppression = false3. getStackTrace() vs printStackTrace()
3.1 getStackTrace() — 编程式访问
public StackTraceElement[] getStackTrace() {
return getOurStackTrace().clone(); // 防御性拷贝
}
private synchronized StackTraceElement[] getOurStackTrace() {
if (stackTrace == NO_STACK_TRACE) {
// 如果 fillInStackTrace() 被跳过,返回空数组
return NO_STACK_TRACE;
}
if (stackTrace == null) {
// 仅剩 JDK 6 之前的反序列化兼容路径
return NO_STACK_TRACE;
}
return stackTrace;
}防御性拷贝:返回 stackTrace.clone() 而非直接返回内部数组引用,防止调用方修改内部状态。
典型用法:
try {
// ...
} catch (Exception e) {
for (StackTraceElement ste : e.getStackTrace()) {
System.out.printf("%s.%s(%s:%d)%n",
ste.getClassName(), ste.getMethodName(),
ste.getFileName(), ste.getLineNumber());
}
}3.2 printStackTrace() — 输出到错误流
public void printStackTrace() {
printStackTrace(System.err);
}
public void printStackTrace(PrintStream s) {
printStackTrace(new WrappingPrintStream(s));
}printStackTrace() 最终委托给内部的 printStackTrace(PrintStreamOrWriter s),其输出格式为:
java.io.IOException: 连接失败
at com.example.App.readFile(App.java:12)
at com.example.App.main(App.java:8)
Caused by: java.net.ConnectException: Connection refused
at java.net.Socket.connect(Socket.java:678)
at com.example.App.openConnection(App.java:20)
... 1 more3.3 对比
| 维度 | getStackTrace() | printStackTrace() |
|---|---|---|
| 返回类型 | StackTraceElement[] | void(输出到流) |
| 用途 | 编程遍历、日志框架使用 | 手动调试、标准错误输出 |
| 线程安全 | 返回拷贝,调用方安全 | 见下一节锁分析 |
| 异常链 | 需自行递归 getCause() | 自动输出 Caused by: 链 |
| 性能 | 较低(数组拷贝) | 较高(I/O + 同步) |
4. printStackTrace(PrintStream) 的锁
private void printStackTrace(PrintStreamOrWriter s) {
// synchronized 锁住输出流对象
Set<Throwable> dejaVu = Collections.newSetFromMap(
new IdentityHashMap<>()); // 防止循环引用
synchronized (s) { // ← 关键锁
printStackTrace(s, dejaVu);
}
}4.1 为什么需要 synchronized (s)
printStackTrace(PrintStream) 的输出涉及多行文本。如果多个线程同时对一个 PrintStream 调用 printStackTrace(),没有锁保护的情况下,输出行可能会互相交错:
线程 A: java.io.IOException: 错误A
线程 B: java.lang.NullPointerException: 错误B
线程 A: at com.example.App.methodA(App.java:10) ← 交错!
线程 A: at com.example.App.methodB(App.java:20)
线程 B: at com.example.App.methodC(App.java:30)而加了 synchronized (s) 后,每个异常的完整栈输出是原子的:
线程 A: java.io.IOException: 错误A
线程 A: at com.example.App.methodA(App.java:10)
线程 A: at com.example.App.methodB(App.java:20)
← 线程 B 在这里等待锁
线程 B: java.lang.NullPointerException: 错误B
线程 B: at com.example.App.methodC(App.java:30)4.2 双重同步的解释
System.err 本身是一个 PrintStream,其内部 println() 等方法也是 synchronized 的:
// java.io.PrintStream
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}既然 PrintStream 的每个 println() 已经是同步的,为什么 printStackTrace() 还要额外加锁?
原因:println() 的锁只能保证单行输出的原子性,而 printStackTrace() 需要保证整个异常栈(多行 + Caused by 链)的原子性。如果没有外层的 synchronized (s),虽然每行不会交错,但不同线程的异常栈行会交替出现,可读性极差。
5. suppressedExceptions 的 try-with-resources 增强
JDK 7 引入了 try-with-resources 语法,同时也带来了"抑制异常"(Suppressed Exception)的概念。
5.1 场景:两个异常同时发生
public class Resource implements AutoCloseable {
@Override
public void close() throws IOException {
throw new IOException("资源关闭失败");
}
}
public void demo() throws IOException {
try (Resource r = new Resource()) {
throw new IOException("主异常"); // try 块抛出主异常
} // close() 也抛出了异常——冲突!
}问题是:try 块抛出了主异常,close() 又抛出了另一个异常。传统做法只能丢弃其中一个,但丢失的信息对排查问题至关重要。
5.2 addSuppressed() 的解决方案
JDK 7 引入了抑制异常机制:主异常"压制"(suppress)掉 close() 抛出的异常,将后者挂载到主异常的 suppressedExceptions 列表中:
// java.lang.Throwable(JDK 7+)
public final synchronized void addSuppressed(Throwable exception) {
if (exception == this)
throw new IllegalArgumentException("Self-suppression not permitted");
if (suppressedExceptions == SUPPRESSED_NO_EXCEPTIONS)
return; // 禁止抑制
if (suppressedExceptions == null)
suppressedExceptions = new ArrayList<>();
suppressedExceptions.add(exception);
}
public final synchronized Throwable[] getSuppressed() {
if (suppressedExceptions == SUPPRESSED_NO_EXCEPTIONS
|| suppressedExceptions == null)
return EMPTY_THROWABLE_ARRAY;
return suppressedExceptions.toArray(EMPTY_THROWABLE_ARRAY);
}try-with-resources 编译后的等价逻辑:
// 编译器生成的等效代码
Throwable primaryException = null;
Resource r = new Resource();
try {
throw new IOException("主异常");
} catch (Throwable t) {
primaryException = t;
throw t;
} finally {
if (r != null) {
if (primaryException != null) {
try {
r.close();
} catch (Throwable suppressed) {
primaryException.addSuppressed(suppressed); // ← 关键
}
} else {
r.close();
}
}
}5.3 最大抑制异常数量
private static final int MAX_SUPPRESSED = 100;addSuppressed() 内部会在添加前检查数量,防止因异常递归嵌套导致的内存泄露:
// JDK 19+ 源码中的防溢出机制
if (suppressedExceptions.size() >= MAX_SUPPRESSED) {
throw new IllegalArgumentException("Too many suppressed exceptions");
}printStackTrace() 输出抑制异常的格式:
java.io.IOException: 主异常
at com.example.App.demo(App.java:10)
[1] 抑制的异常: java.io.IOException: 资源关闭失败
at com.example.Resource.close(Resource.java:5)
at com.example.App.demo(App.java:8)6. StackTraceElement 的 4 个字段
// java.lang.StackTraceElement(final 不可变类)
public final class StackTraceElement implements java.io.Serializable {
private final String declaringClass; // 声明该方法的类名
private final String methodName; // 方法名
private final String fileName; // 源文件名(可能为 null)
private final int lineNumber; // 行号(负数表示 native 方法)
// ... hashCode/equals 基于所有字段
}6.1 字段说明
| 字段 | 类型 | 说明 | 可能的值 |
|---|---|---|---|
declaringClass | String | 声明该方法的类的全限定名 | "java.lang.String" |
methodName | String | 方法名 | "indexOf" |
fileName | String | 源文件名(没有调试信息时为 null) | "String.java" 或 null |
lineNumber | int | 行号(负值表示特殊含义) | 正数、-1(native)、-2(未知源) |
6.2 不可变性设计
StackTraceElement 是一个 不可变类(所有字段均为 final),这是有意的设计决策:
- 作为异常栈的"快照"元素,不应在异常传播过程中被修改
- 可安全地在多个线程间共享
equals()和hashCode()基于所有 4 个字段计算,可用于去重
6.3 isNativeMethod() 检测
public boolean isNativeMethod() {
return lineNumber == -2; // 负二表示 native 方法
}JDK 中的约定:
lineNumber >= 0:普通 Java 源码行号lineNumber == -1:没有行号调试信息(Unknown Source)lineNumber == -2:native 方法(JDK 7+)
7. StackTraceElement.toString() 的格式
// java.lang.StackTraceElement
public String toString() {
String s = declaringClass + "." + methodName;
if (isNativeMethod()) {
s += "(Native Method)";
} else if (fileName != null && lineNumber >= 0) {
s += "(" + fileName + ":" + lineNumber + ")";
} else if (fileName != null) {
s += "(" + fileName + ")";
} else {
s += "(Unknown Source)";
}
return s;
}7.1 三种格式对照
| 场景 | 条件 | 输出示例 |
|---|---|---|
| 普通方法 | fileName != null && lineNumber >= 0 | java.lang.String.indexOf(String.java:1184) |
| Native 方法 | isNativeMethod() 返回 true | java.lang.Object.hashCode(Native Method) |
| 未知源 | fileName == null | com.example.App.run(Unknown Source) |
7.2 与 printStackTrace() 输出格式的关系
printStackTrace() 内部实际上就是迭代 StackTraceElement[],对每个元素调用 toString(),并在前面加上 \tat 前缀:
\tat java.lang.String.indexOf(String.java:1184)
\tat com.example.App.method(App.java:42)
\tat com.example.App.main(App.java:10)因此,StackTraceElement.toString() 的格式直接决定了 printStackTrace() 的输出风格。
8. 异常处理在 JVM 层面的实现
Java 源代码中的 try-catch-finally 在 JVM 层面被编译为 Exception table(异常表),这是一个存储在 .class 文件中的结构化数据。
8.1 Exception table 的结构
考虑以下 Java 代码:
public void readFile(String path) {
try {
FileInputStream fis = new FileInputStream(path); // PC 5
int data = fis.read(); // PC 12
} catch (IOException e) { // Handler PC 20
log.error("IO错误", e);
} catch (Exception e) { // Handler PC 37
log.error("未知错误", e);
} finally {
close(); // Handler PC 50
}
}编译后的 .class 文件包含:
Exception table:
from to target type
5 17 20 <Class java.io.IOException>
5 17 37 <Class java.lang.Exception>
5 50 50 <Class java.lang.Throwable> ← finally 的"任何异常"表项| 列 | 含义 |
|---|---|
from | try 块起始字节码偏移(PC) |
to | try 块结束字节码偏移(不包含) |
target | catch 块起始字节码偏移(handler_pc) |
type | 捕获的异常类型(null 表示 finally,即捕获所有异常) |
8.2 finally 的编译策略:代码复制
JDK 7 之前,finally 块通过 jsr(Jump Subroutine)和 ret 指令实现。JDK 7 之后,编译器将 finally 块的代码内联复制到两个位置:
正常路径:try 块结束 → finally 代码 → 继续后续
异常路径:catch 块结束 → finally 代码 → 抛出异常
未捕获的异常 → finally 代码 → 重新抛出编译后伪代码流:
┌────────────────────┐
│ try 块代码 │
│ (PC 5 ~ PC 17) │
└────────┬───────────┘
│
┌─────────────┼─────────────┐
│ 正常结束 │ 抛出异常 │
▼ ▼ │
┌──────────────┐ ┌──────────┐ │
│ finally 代码 │ │ catch │ │
│ (PC 50~60) │ │ (handler)│ │
└──────┬───────┘ └────┬─────┘ │
▼ ▼ │
┌────────────┐ ┌──────────┐ │
│ 继续执行 │ │ finally │◄──────┘
└────────────┘ │ (代码复制)│
└────┬─────┘
▼
┌────────────┐
│ 重新抛出 │
└────────────┘这种"代码复制"策略避免了复杂的 jsr/ret 字节码,也简化了 JVM 的栈帧管理。
8.3 JVM 异常查找流程
当 JVM 执行到 athrow 指令(抛出异常)时,按以下流程查找处理器:
1. 当前 PC 落入某个异常表项的 [from, to) 范围?
├── 是 → 异常类型匹配(instanceof 检查)?
│ ├── 是 → 跳转到 target_pc(handler),执行 catch 块
│ └── 否 → 继续查找下一个表项
└── 否 → 当前方法无匹配处理器
↓
2. 展开当前栈帧(pop 当前方法栈帧)
↓
3. 返回到调用方法的抛出点(PC)
↓
4. 重复步骤 1(在调用方法中查找 Exception table)
↓
...直到找到匹配的处理器或到达线程入口
↓
未找到处理器 → 线程终止(打印未捕获异常到 System.err)// hotspot/share/runtime/continuationFreezer.cpp 中的查找逻辑(简化)
void methodHandle::catch_handler_for_exception(JavaThread* thread, ...) {
// 遍历 Exception table
for (int i = 0; i < exception_table_length(); i++) {
// 检查 PC 是否在 [from, to) 范围内
if (pc >= exception_table_start[i].from &&
pc < exception_table_start[i].to) {
// 检查异常类型是否匹配
Klass* catch_class = exception_table_start[i].catch_type();
if (exception_class->is_subtype_of(catch_class)) {
// 匹配成功,跳转到 handler_pc
return exception_table_start[i].handler_pc();
}
}
}
// 未匹配,展开栈帧
}8.4 异常匹配的顺序
JVM 按 Exception table 中的顺序依次匹配。这意味着 catch (IOException e) 必须写在 catch (Exception e) 之前——如果先写 catch (Exception),IOException 会被 Exception 表项匹配到,后面的 catch (IOException) 永远不会被访问。这就是为什么 Java 编译器要求子类异常必须写在父类异常之前。
9. 异常体系关键类结构一览
java.lang.Throwable (implements Serializable)
├── detailMessage: String ← 错误描述文本
├── cause: Throwable ← 链式异常的根本原因
├── stackTrace: StackTraceElement[] ← 调用栈快照(fillInStackTrace)
├── suppressedExceptions: List<Throwable> ← 抑制异常(JDK 7+)
├── writableStackTrace: boolean ← 是否跳过填栈(JDK 7+)
│
├── Throwable() ← fillInStackTrace()
├── Throwable(message) ← fillInStackTrace() + detailMessage
├── Throwable(message, cause) ← fillInStackTrace() + detailMessage + cause
├── Throwable(message, cause, enableSuppression, writableStackTrace) ← JDK 7+
│
├── fillInStackTrace() ← native,快照调用栈
├── getMessage() ← 返回 detailMessage
├── getCause() ← 返回 cause(null 表示无原因)
├── initCause(Throwable) ← 延迟设置 cause
├── getStackTrace() ← 返回 stackTrace 的防御性拷贝
├── printStackTrace() ← 输出到 System.err
├── printStackTrace(PrintStream) ← synchronized(s) 保证输出原子性
├── addSuppressed(Throwable) ← try-with-resources 使用
└── getSuppressed() ← 返回抑制异常数组
│
├── java.lang.Exception ← 检查型异常基类
│ ├── java.lang.RuntimeException ← 非检查型异常基类
│ │ ├── NullPointerException
│ │ ├── IllegalArgumentException
│ │ ├── IndexOutOfBoundsException
│ │ └── ...
│ ├── java.io.IOException
│ ├── java.sql.SQLException
│ └── ...
│
└── java.lang.Error ← 不可恢复错误
├── OutOfMemoryError
├── StackOverflowError
├── VirtualMachineError
└── ...
java.lang.StackTraceElement (final, immutable)
├── declaringClass: String ← 类名
├── methodName: String ← 方法名
├── fileName: String ← 文件名(可能 null)
├── lineNumber: int ← 行号(≥0 普通,-2 native,-1 未知)
└── toString() ← className.methodName(fileName:lineNumber)检查型异常 vs 非检查型异常
| 维度 | 检查型异常(Checked Exception) | 非检查型异常(Unchecked Exception) |
|---|---|---|
| 父类 | Exception(除 RuntimeException) | RuntimeException 或 Error |
| 编译期检查 | 必须 throws 或 catch | 无需声明 |
| 典型场景 | I/O 错误、SQL 错误、类不存在 | 空指针、数组越界、除零 |
| 设计意图 | 可恢复的外部错误 | 编程错误或系统级错误 |
Objects.requireNonNull() 与 NullPointerException 增强(JDK 14+)
// java.util.Objects
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
public static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}JDK 14 引入的 JEP 358 改进了 NullPointerException,在 JVM 层面精确指出哪个变量为 null:
public class NpeDemo {
public static void main(String[] args) {
User user = null;
System.out.println(user.getName()); // JDK 14+: 明确指出 user 为 null
}
}
// JDK 14 之前:
// Exception in thread "main" java.lang.NullPointerException
// at NpeDemo.main(NpeDemo.java:4)
//
// JDK 14+(-XX:+ShowCodeDetailsInExceptionMessages):
// Exception in thread "main" java.lang.NullPointerException:
// Cannot invoke "User.getName()" because "user" is null
// at NpeDemo.main(NpeDemo.java:4)总结
- 3 个核心属性构成了 Throwable 状态模型:
detailMessage描述错误,cause构建异常链,stackTrace记录调用栈快照 fillInStackTrace()的 native 实现通过 JVM 遍历栈帧收集StackTraceElement[],性能随栈深度线性增长;可通过writableStackTrace = false跳过getStackTrace()返回防御性拷贝供编程遍历,printStackTrace()使用synchronized (s)保证多行异常栈输出的原子性suppressedExceptions解决了try-with-resources中主异常与close()异常的冲突问题,最多保存 100 个抑制异常StackTraceElement不可变设计包含 4 个字段,toString()决定printStackTrace()的输出格式- JVM 层通过 Exception table 实现异常处理,
finally采用代码复制策略,异常查找按顺序匹配并在未命中时展开栈帧 - 继承体系:
Throwable→Exception(检查型)|RuntimeException(非检查型)|Error(系统级错误)