我们在编程时遇到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);

更多推荐