JAVA异常信息是如何产生并输出在控制台的
我们在编程时遇到error时,控制台总是会弹出类似下图的报错信息

除了第一行的exception名称和具体解释,我们看到在下方还有三行问题定位信息,点击括号内的定位即可跳转对应错误代码。
那么,这三行定位信息是如何显示,又是为什么会显示呢?
先看java的异常监控区域try-catch-finally。
try {
// code
} catch (Exception e){
e.printStackTrace();
} finally {
// action happen every time
}
我们看到在catch块中调用的方法叫e.printStackTrace() ,意思就是打印栈轨迹,正是这个方法打印了上文所说的方法定位信息。
点开e.printStackTrace() 的源码
public void printStackTrace() {
printStackTrace(System.err);
}
/**
* Prints this throwable and its backtrace to the specified print stream.
*
* @param s {@code PrintStream} to use for output
*/
public void printStackTrace(PrintStream s) {
printStackTrace(new WrappedPrintStream(s));
}
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);
}
}
虽然是三个重载方法,但是前两个都是调用的同名方法,仅传入默认值。直接看第三个重载方法。
首先使用了IdentityHashMap,这个Map区别于普通HashMap的地方在于它使用==判断两个对象是否相等,而不是.equals(),好处在于防止有人通过重写equals方法绕过去重检查,造成异常链循环调用无限递归。然后用锁锁住了线程,打印了stackTrace,并且检查有无被抑制的异常和异常原因,如果有也打印。我们着重看打印stackTrace的部分,可以看到先使用this指针调用对象的toString方法,打印异常对象e,这就是第一行信息:Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 2 out of bounds for length 1。然后调用了getOurStackTrace()方法调取stackTrace并输出。
接着看getOurStackTrace() 方法的源代码。
private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
private StackTraceElement[] stackTrace = UNASSIGNED_STACK;
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;
}
整个方法是懒加载缓存设计。UNASSIGNED_STACK是一个static final的初始化空数组哨兵,stackTrace在异常刚创建的时候也指向了UNASSIGNED_STACK。这里先判断二者地址是否相等,实质是判断异常是否为初次遇到(即本异常对象的stackTrace没有填入数据),如果stackTrace没有指向UNASSIGNED_STACK并且不为null说明缓存命中,直接输出stackTrace内保存的堆栈帧。如果stackTrace指向了UNASSIGNED_STACK,则说明需要访问堆栈调取帧,则调用StackTraceElement.of 方法,并填入stackTrace内。
那么就会有人想问了,异常信息难道不是一次性的吗?运行程序,遇到异常,打印在控制台,程序中止,为什么还要设计缓存机制呢?诶,其实实际上异常对象经常被多次读取栈帧,缓存复用的情况很是常见。比如上文提到的pringStackTrace方法内用于输出打印被抑制异常和异常原因的printEnclosedStackTrace方法,因为涉及到去重,需要比对每一个栈帧,在for循环内如果每循环一次就要调取一次,性能开销就会非常大。而使用缓存则可以直接调取,无需再访问堆栈。此外,日志管理、监控框架等多个框架同时处理同一个异常时也会涉及多次调用,异常信息并不是仅仅输出在控制台这么简单的。
真正的访问堆栈帧并输出到数组发生在 StackTraceElement.of(this, depth) 这一行调用里。
StackTraceElement.of方法调用了一个java native方法initStackTraceElements,该方法用c++写成,直接和底层交互,拿到堆栈帧。
/*
* Returns an array of StackTraceElements of the given depth
* filled from the backtrace of a given Throwable.
*/
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
initStackTraceElements(stackTrace, x);
// ensure the proper StackTraceElement initialization
for (StackTraceElement ste : stackTrace) {
ste.computeFormat();
}
return stackTrace;
}
/*
* Sets the given stack trace elements with the backtrace
* of the given Throwable.
*/
private static native void initStackTraceElements(StackTraceElement[] elements,
Throwable x);
更多推荐
所有评论(0)