Throwable类源码剖析

Throwable类源码剖析

一、Throwable类的简介

Throwable类是Java语言中所有ErrorException的超类。只有作为该类(或其子类之一)的对象才会被Java虚拟机抛出,或被Java throw语句抛出。同样,只有本类或其子类之一可以作为catch子句的参数类型。为了对异常进行编译时检查,Throwable和 Throwable的任何子类(如果它们不是RuntimeExceptionError的子类)都被视为检查时异常

ErrorException这两个子类的实例通常用于表示发生了异常情况。通常,这些实例是在异常情况的上下文中新创建的,以便包含相关信息(如==堆栈跟踪数据==)。

Throwable包含其线程在创建时执行堆栈的快照。 它还可以包含一个消息字符串,提供有关错误的更多信息。 随着时间的推移,一个 Throwable可以抑制其他Throwable的传播。 最后,该Throwable还可以包含一个cause(类型也是Throwable):即另一个Throwable导致该Throwable被构造。这种因果信息的记录被称为链式异常设施,因为cause本身可以有一个cause,依此类推,导致异常“链”,每个异常都是由另一个异常引起的。

Throwable有cause的可能原因之一是抛出它的类是构建在较低层抽象之上的,并且上层的操作会因为下层中的故障而失败。==让下层抛出的Throwable向上传播是一个糟糕的设计,因为它通常与上层提供的抽象无关。==此外,假设下层的异常是检查时异常,这样做会将上层的API与其实现的细节联系起来。抛出“包装异常”(即包含cause的异常)允许上层向其调用者传达失败的详细信息,而不会产生这些缺点。它保留了更改上层实现的灵活性,而无需更改其API(特别是其方法抛出的异常集)。

Throwable有cause的第二个可能原因是抛出它的方法必须符合通用接口并且该通用接口不允许该方法直接抛出cause。例如,假设持久性集合符合Collection接口,并且其持久性是在java.io之上实现的。假设add方法的内部可能会抛出IOException,通过将IOException包装在适当的非检查时异常中,可以将IOException的详细信息传递给其调用者,同时符合Collection接口。(持久集合的规范应该表明它能够抛出此类异常。)

按照惯例,Throwable类及其子类有两个构造函数,一个不带参数,另一个带可用于生成详细消息的String参数。 此外,那些可能有与之相关的cause的子类应该有两个以上的构造函数,一个采用Throwable(cause),另一个采用String(详细消息)和Throwable(cause)两个传入参数。

image-20230918132921407

二、Throwable类的重要字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 由native方法赋值,用来保存栈信息的轨迹
// The JVM saves some indication of the stack backtrace in this slot.
private transient Object backtrace;
// The JVM code sets the depth of the backtrace for later retrieval
private transient int depth;
// 异常描述信息,如RuntimeException("File Not Found"),其中字符串"File Not Found"就是异常描述信息
private String detailMessage;
// 记录当前异常是由哪个异常所引起的,默认是this,可通过构造器自定义,也可以通过initCase方法进行修改
private Throwable cause = this;
// 引起异常的堆栈跟踪信息,初始化为零长度数组,该字段为null意味着随后setStackTrace(StackTraceElement[])和fillInStackTrace()方法什么也不做
private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
private StackTraceElement[] stackTrace = UNASSIGNED_STACK;
// 该列表初始化为零元素不可修改的哨兵列表。读入序列化的Throwable时,如果suppressedExceptions字段指向零元素列表,则该字段将重置为哨兵值(SUPPRESSED_SENTINEL)。
private static final List<Throwable> SUPPRESSED_SENTINEL = Collections.emptyList();
private List<Throwable> suppressedExceptions = SUPPRESSED_SENTINEL;

为了允许Throwable对象不可变并被JVM安全地重用,例如OutOfMemoryErrors,可写的Throwable字段以响应用户操作、cause、stackTrace和suppressedExceptions遵循以下协议:

  1. 这些字段初始化为非空哨兵值(SENTINEL),这表示该值在逻辑上尚未设置。
  2. null写入这些字段表示禁止后面进一步写入。
  3. 字段的哨兵值可以替换为另一个非空值。

我们简单看一下StackTraceElement类的信息,下面是截取的一部分内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class StackTraceElement implements java.io.Serializable {
// Normally initialized by VM
private String classLoaderName;
private String moduleName;
private String moduleVersion;
// 类名
private String declaringClass;
// 方法名
private String methodName;
// 文件名
private String fileName;
// 行号
private int lineNumber;
private byte format = 0; // Default to show all
}

三、Throwable类的构造方法

该构造方法创建一个message为null的Throwable类,cause字段还未初始化(可能随后由initCause方法初始化)。

1
2
3
public Throwable() {
fillInStackTrace();
}

与上一个类似,不过声明了该Throwable类的异常描述信息message,fillInStackTrace用来初始化栈追踪信息。

1
2
3
4
public Throwable(String message) {
fillInStackTrace();
detailMessage = message;
}

与上一个类似,不过声明了该Throwable类的产生原因cause,注意cause允许为null,此时表示cause不存在或者未知

1
2
3
4
5
public Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this.cause = cause;
}

该构造方法参数只有cause,detailMessage根据cause推导得出:cause==null ? null : cause.toString()

1
2
3
4
5
public Throwable(Throwable cause) {
fillInStackTrace();
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}

image-20230918141302199

1
2
3
4
5
6
7
8
9
10
11
12
13
protected Throwable(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
if (writableStackTrace) {
fillInStackTrace();
} else {
stackTrace = null;
}
detailMessage = message;
this.cause = cause;
if (!enableSuppression)
suppressedExceptions = null;
}

Throwable提供了4个public构造器和1个protected构造器(该构造器由JDK1.7引入)。4个public构造器共同点就是都调用了fillInStackTrace方法。

四、fillInStackTrace方法

fillInStackTrace会首先判断stackTrace是不是为null,如果不为null则会调用native方法fillInStackTrace(0)。那么什么时候为null呢,答案是上面的protected构造器可以指定writableStackTrace为false,这样stackTrace就为null了,就不会调用fillInStackTrace获取堆栈信息。==fillInStackTrace将当前线程的栈帧信息记录到此Throwable中==。

1
2
3
4
5
6
7
8
9
10
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
fillInStackTrace(0);
stackTrace = UNASSIGNED_STACK;
}
return this;
}

private native Throwable fillInStackTrace(int dummy);

fillInStackTrace调用结束后JVM会设置好backtracedepth的值,stackTrace重新赋值为哨兵值UNASSIGNED_STACK。

未调用获取栈信息或者打印栈信息的方法前,stackTrace都是哨兵值!

下面是对于fillInStackTrace方法功能的验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package test.lang;

public class MyException extends RuntimeException{
public static void method1() {
System.out.println("method1");
method2();
}

public static void method2() {
System.out.println("method2");
method3();
}

public static void method3() {
throw new MyException();
}

public static void main(String[] args) {
method1();
}
}

image-20230918144453232

如果重写fillInStackTrace方法后,会发现输出的结果中没有栈追踪信息了,验证了fillInStackTrace作用是将当前线程的栈帧信息记录到此Throwable中

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
package test.lang;

public class MyException extends RuntimeException{

@Override
public synchronized Throwable fillInStackTrace() {
return this;
}

public static void method1() {
System.out.println("method1");
method2();
}

public static void method2() {
System.out.println("method2");
method3();
}

public static void method3() {
throw new MyException();
}

public static void main(String[] args) {
method1();
}
}

image-20230918145008709

五、getStackTrace方法

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
public StackTraceElement[] getStackTrace() {
return getOurStackTrace().clone();
}

private synchronized StackTraceElement[] getOurStackTrace() {
// Initialize stack trace field with information from
// backtrace if this is the first call to this method
if (stackTrace == UNASSIGNED_STACK ||
(stackTrace == null && backtrace != null) /* Out of protocol state */) {
stackTrace = StackTraceElement.of(this, depth);
} else if (stackTrace == null) {
return UNASSIGNED_STACK;
}
return stackTrace;
}

static StackTraceElement[] of(Throwable x, int depth) {
// 创建栈追踪信息数组
StackTraceElement[] stackTrace = new StackTraceElement[depth];
for (int i = 0; i < depth; i++) {
stackTrace[i] = new StackTraceElement();
}

// VM to fill in StackTraceElement
// 利用给定Throwable类的backtrace字段设置栈追踪信息数组元素,本地方法
initStackTraceElements(stackTrace, x);

// ensure the proper StackTraceElement initialization
for (StackTraceElement ste : stackTrace) {
ste.computeFormat();
}
return stackTrace;
}

六、initCause方法

initCause方法初始化Throwable类的cause字段,该方法最多调用一次,它通常从构造函数内部调用,或者在创建Throwable对象后立即调用。如果Throwable类通过Throwable(Throwable)或者Throwable(String, Throwable)构造得到,则initCause方法再也不能调用。

1
2
3
4
5
6
7
8
9
public synchronized Throwable initCause(Throwable cause) {
if (this.cause != this)
throw new IllegalStateException("Can't overwrite cause with " +
Objects.toString(cause, "a null"), this);
if (cause == this)
throw new IllegalArgumentException("Self-causation not permitted", this);
this.cause = cause;
return this;
}

下面是一个使用的例子:

1
2
3
4
5
6
try {
lowLevelOp();
} catch (LowLevelException le) {
throw (HighLevelException)
new HighLevelException().initCause(le); // Legacy constructor
}

七、常用的简单方法

image-20230918150137007

1
2
3
public String getMessage() {
return detailMessage;
}

image-20230918150407136

1
2
3
public String getLocalizedMessage() {
return getMessage();
}

image-20230918150625943

1
2
3
public synchronized Throwable getCause() {
return (cause==this ? null : cause);
}

image-20230918150730036

1
2
3
4
5
public String toString() {
String s = getClass().getName();
String message = getLocalizedMessage();
return (message != null) ? (s + ": " + message) : s;
}

八、addSuppressed()和getSuppressed()方法

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
private static final String SELF_SUPPRESSION_MESSAGE = "Self-suppression not permitted";
private static final String NULL_CAUSE_MESSAGE = "Cannot suppress a null exception.";

public final synchronized void addSuppressed(Throwable exception) {
// 附加的exception不能为自身
if (exception == this)
throw new IllegalArgumentException(SELF_SUPPRESSION_MESSAGE, exception);
// 附加的exception不能为null
if (exception == null)
throw new NullPointerException(NULL_CAUSE_MESSAGE);
// 不允许设置suppressedExceptions
if (suppressedExceptions == null) // Suppressed exceptions not recorded
return;
// suppressedExceptions初始化
if (suppressedExceptions == SUPPRESSED_SENTINEL)
suppressedExceptions = new ArrayList<>(1);

suppressedExceptions.add(exception);
}

private static final Throwable[] EMPTY_THROWABLE_ARRAY = new Throwable[0];
public final synchronized Throwable[] getSuppressed() {
if (suppressedExceptions == SUPPRESSED_SENTINEL ||
suppressedExceptions == null)
return EMPTY_THROWABLE_ARRAY;
else
return suppressedExceptions.toArray(EMPTY_THROWABLE_ARRAY);
}

如果try中抛出了异常,在执行流程转移到方法栈上一层之前,finally语句块会执行,但是,如果在finally语句块中又抛出了一个异常,那么这个异常会覆盖掉之前抛出的异常,这点很像finally中return的覆盖。比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
package test.lang;

public class ThrowableTest {
public static void main(String[] args) {
try {
Integer i = Integer.valueOf("hhh");
} catch (NumberFormatException e) {
throw new RuntimeException("hhh",e);
} finally {
throw new RuntimeException("www");
}
}
}

image-20230918152621114

Throwable对象提供了addSupperssed和getSupperssed方法,允许把finally语句块中产生的异常通过addSupperssed方法添加到try语句产生的异常中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package test.lang;

public class ThrowableTest {
public static void main(String[] args) {
// Throwable exception = new Exception().initCause(new RuntimeException());
RuntimeException exception = null;
try {
Integer i = Integer.valueOf("hhh");
} catch (NumberFormatException e) {
exception = new RuntimeException("hhh",e);
throw exception;
} finally {
RuntimeException exception1 = new RuntimeException("www");
exception.addSuppressed(exception1);
throw exception;
}
}
}

image-20230918152204832

九、printStackTrace方法

printStackTrace()方法分四个方面打印出当前异常信息

  • 打印出当前异常的详细信息

  • 打印出异常堆栈中的栈帧信息

  • 打印出suppressed异常信息

  • 递归打印出引起当前异常的异常信息

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
public void printStackTrace() {
printStackTrace(System.err);
}

public void printStackTrace(PrintStream s) {
printStackTrace(new WrappedPrintStream(s));
}

private static final String SUPPRESSED_CAPTION = "Suppressed: ";
private static final String CAUSE_CAPTION = "Caused by: ";

private void printStackTrace(PrintStreamOrWriter s) {
// Guard against malicious overrides of Throwable.equals by
// using a Set with identity equality semantics.
Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<>());
dejaVu.add(this);

synchronized (s.lock()) {
// Print our stack trace
s.println(this);
StackTraceElement[] trace = getOurStackTrace();
for (StackTraceElement traceElement : trace)
s.println("\tat " + traceElement);

// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);

// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
}
}

参考之前的异常打印截图,逐一对比发现逻辑一致,先后打印四个方面的信息。