本文详细总结了stack trace的各个部分的含义和使用方法,同时深入介绍了一些与异常相关的函数和最佳实践。

参考资料

  1. What’s a Java Stack Trace?
  2. Understanding and Leveraging the Java Stack Trace
  3. 如何看异常堆栈信息
  4. Java Stack Trace: How to Read and Understand to Debug Code
  5. Creating and reading stacktraces
  6. Understanding Exception Stack Trace in Java with Code Examples

Call Stack 函数调用栈 与 stack trace

The Stack, more accurately called the runtime or call stack, is a set of stack frames a program creates as it executes, organized in a stack data structure.

函数调用栈是一个以栈的形式保存从程序开始到运行当时调用的所有函数栈的结构。

Simply put, a stack trace is a representation of a call stack at a certain point in time, with each element representing a method invocation. The stack trace contains all invocations from the start of a thread until the point it’s generated. This is usually a position at which an exception takes place.

Stack trace 则是call stack的一种展示形式,打印出函数的名字与相关代码行。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StackTrace {

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

static void a() {
b();
}

static void b() {
c();
}

static void c() {
d();
}

static void d() {
Thread.dumpStack();
}
}
1
2
3
4
5
6
7
java.lang.Exception: Stack trace
at java.base/java.lang.Thread.dumpStack(Thread.java:1383)
at com.ericgoebelbecker.stacktraces.StackTrace.d(StackTrace.java:23)
at com.ericgoebelbecker.stacktraces.StackTrace.c(StackTrace.java:19)
at com.ericgoebelbecker.stacktraces.StackTrace.b(StackTrace.java:15)
at com.ericgoebelbecker.stacktraces.StackTrace.a(StackTrace.java:11)
at com.ericgoebelbecker.stacktraces.StackTrace.main(StackTrace.java:7)

栈信息从上往下读,最上面的是最后调用的函数,main调用a,a调用b,b调用c,c调用d,d调用dumpStack,这个函数输出以上信息。

A Java stack trace is a snapshot of a moment in time. You can see where your application was and how it got there. That’s valuable insight that you can use a few different ways.

Java Exception

Stack traces and exceptions are often associated with each other. When you see a Java application throw an exception, you usually see a stack trace logged with it. This is because of how exceptions work.

When Java code throws an exception, the runtime looks up the stack for a method that has a handler that can process it. If it finds one, it passes the exception to it. If it doesn’t, the program exits. So exceptions and the call stack are linked directly. Understanding this relationship will help you figure out why your code threw an exception.

Java Exception机制通常是和call stack函数调用栈联系在一起的。当某个函数抛出了一个异常,JVM会查看调用栈,看哪个函数可以处理这个异常(有catch),若找到则把异常传过去,若没有则终止程序。

捕捉到异常后可以执行不同的操作,一个简单的选择是直接输出这个异常的信息:

1
2
3
4
5
try {
// ...
} catch (NullPointerException e) {
System.err.println(e.getMessage());
}

若没有能够处理这个异常的函数,异常会被JVM接管,JVM会输出它抛出地的函数调用栈信息:

1
2
3
4
5
6
Exception in thread "main" java.lang.NullPointerException
at com.ericgoebelbecker.stacktraces.StackTrace.d(StackTrace.java:29)
at com.ericgoebelbecker.stacktraces.StackTrace.c(StackTrace.java:24)
at com.ericgoebelbecker.stacktraces.StackTrace.b(StackTrace.java:20)
at com.ericgoebelbecker.stacktraces.StackTrace.a(StackTrace.java:16)
at com.ericgoebelbecker.stacktraces.StackTrace.main(StackTrace.java:9)

BUT 输出了这个信息不代表这个异常就一定没有被处理catch,一种情况是catch了以后又抛出了:

image-20210709122348187

另一种情况是我们自己处理的时候输出了这个信息,只需要调用异常的printStackTrace()方法即可:

1
2
3
4
5
try {
// ...
} catch (NullPointerException e) {
e.printStackTrace();
}

image-20210709122607629

即使捕捉异常的操作是在main中执行的,它仍会打印出抛出地的调用栈信息,这有利于我们找到异常的源头。

这两种情况打印的内容有些不同:没有处理而终止的异常打印时开头多了一行内容:

1
Exception in thread "main"

stack trace各部分具体介绍

原文:参考资料4

The first line tells us the details of the Exception:

Example java stack trace

This is a good start. Line 2 shows what code was running when that happened:

how to read stack trace

That helps us narrow down the problem, but what part of the code called badMethod? The answer is on the next line down, which can be read in the exact same way. And how did we get there? Look on the next line. And so on, until you get to the last line, which is the main method of the application. Reading the stack trace from bottom to top you can trace the exact path from the beginning of your code, right to the Exception.

将stack trace记录在log中而不是打印在控制台

使用Log4j或者Logback时,可以通过error函数保存异常的调用栈信息:

1
logger.error(“Something bad happened:”, e);

日志中的内容将会是:

1
2
3
4
5
6
7
Something bad happened:
java.lang.NullPointerException: Oops!
at com.ericgoebelbecker.stacktraces.StackTrace.d(StackTrace.java:28)
at com.ericgoebelbecker.stacktraces.StackTrace.c(StackTrace.java:24)
at com.ericgoebelbecker.stacktraces.StackTrace.b(StackTrace.java:20)
at com.ericgoebelbecker.stacktraces.StackTrace.a(StackTrace.java:16)
at com.ericgoebelbecker.stacktraces.StackTrace.main(StackTrace.java:9)

深入stack trace

StackTraceElement class

这个类的每一个实例代表了stack trace中的一个元素。

API:

  • getClassName – returns the fully qualified name of the class containing the method invocation
  • getMethodName – returns the name of the method containing the method invocation
  • getFileName – returns the name of the source file associated with the class containing the method invocation
  • getLineNumber – returns the line number of the source line containing the execution point

更多查看 Java API documentation

Thread class

我们还可以通过调用Thread实例的getStackTrace()方法从线程得到了其stack trace。这个方法返回一个包含StackTraceElement实例的数组。

示例:

1
2
3
4
5
6
7
8
public StackTraceElement[] methodA() {
return methodB();
}

public StackTraceElement[] methodB() {
Thread thread = Thread.currentThread();
return thread.getStackTrace();
}
1
2
3
4
5
6
7
@Test
public void whenElementOneIsReadUsingThread_thenMethodUnderTestIsObtained() {
StackTraceElement[] stackTrace = new StackElementExample().methodA();
StackTraceElement elementOne = stackTrace[1];
assertEquals("com.stackify.stacktrace.StackElementExample", elementOne.getClassName());
assertEquals("methodB", elementOne.getMethodName());
}

stackTrace[0]是getStackTrace这个方法本身的调用。

Throwable class

除了可以对Exception e进行打印stack trace的操作(printStackTrace函数),我们还可以通过getStackTrace函数获得与Thread中相同的一个StackTraceElement数组:

1
2
3
4
5
6
7
8
9
10
11
12
public StackTraceElement[] methodC() {
try {
methodD();
} catch (Throwable t) {
return t.getStackTrace();
}
return null;
}

public void methodD() throws Throwable {
throw new Throwable("A test exception");
}
1
2
3
4
5
6
7
@Test
public void whenElementZeroIsReadUsingThrowable_thenMethodThrowingThrowableIsObtained() {
StackTraceElement[] stackTrace = new StackElementExample().methodC();
StackTraceElement elementZero = stackTrace[0];
assertEquals("com.stackify.stacktrace.StackElementExample", elementZero.getClassName());
assertEquals("methodD", elementZero.getMethodName());
}

打印异常堆栈信息的函数 printStackTrace

printStackTrace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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<Throwable, Boolean>());
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);
}
}

printStackTrace内部调用printEnclosedStackTrace:

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
35
36
37
38
39
40
41
/**
* Print our stack trace as an enclosed exception for the specified
* stack trace.
*/
private void printEnclosedStackTrace(PrintStreamOrWriter s,
StackTraceElement[] enclosingTrace,
String caption,
String prefix,
Set<Throwable> dejaVu) {
assert Thread.holdsLock(s.lock());
if (dejaVu.contains(this)) {
s.println("\t[CIRCULAR REFERENCE:" + this + "]");
} else {
dejaVu.add(this);
// Compute number of frames in common between this and enclosing trace
StackTraceElement[] trace = getOurStackTrace();
int m = trace.length - 1;
int n = enclosingTrace.length - 1;
while (m >= 0 && n >=0 && trace[m].equals(enclosingTrace[n])) {
m--; n--;
}
int framesInCommon = trace.length - 1 - m;

// Print our stack trace
s.println(prefix + caption + this);
for (int i = 0; i <= m; i++)
s.println(prefix + "\tat " + trace[i]);
if (framesInCommon != 0)
s.println(prefix + "\t... " + framesInCommon + " more");

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

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

信息中的… n more是怎么来的

原文:参考资料3

示例:

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
public class TestEx {
private void fun1() throws IOException {
throw new IOException("level 1 exception");
}
private void fun2() throws IOException {
try {
fun1();
System.out.println("2");
} catch (IOException e) {
throw new IOException("level 2 exception", e);
}
}
private void fun3() {
try {
fun2();
System.out.println("3");
} catch (IOException e) {
throw new RuntimeException("level 3 exception", e);
}
}
public static void main(String[] args) {
try {
new TestEx().fun3();
System.out.println("0");
} catch (Exception e) {
e.printStackTrace();
}
}
}

调试输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
Connected to the target VM, address: '127.0.0.1:57353', transport: 'socket'
java.lang.RuntimeException: level 3 exception
at person.ismallboy.console.TestEx.fun3(TestEx.java:24)
at person.ismallboy.console.TestEx.main(TestEx.java:30)
Caused by: java.io.IOException: level 2 exception
at person.ismallboy.console.TestEx.fun2(TestEx.java:15)
at person.ismallboy.console.TestEx.fun3(TestEx.java:21)
... 1 more
Caused by: java.io.IOException: level 1 exception
at person.ismallboy.console.TestEx.fun1(TestEx.java:7)
at person.ismallboy.console.TestEx.fun2(TestEx.java:12)
... 2 more
Disconnected from the target VM, address: '127.0.0.1:57353', transport: 'socket'

printEnclosedStackTrace函数其实是一个回调输出堆栈的过程。隐藏部分堆栈,是为了提高性能,省略一些不必要的输出,输出的内容越多,io耗时越慢。

The trace ends with an “… N more” which indicates that the last N frames are the same as for the previous exception.

其实“… n more”的部分是重复的堆栈部分。我们分析一下上面这个函数“printEnclosedStackTrace”,翻译为“打印封闭堆栈跟踪信息”,“封闭”暂且可以理解为“完整的”,这个函数有两个比较重要的变量,分别是“enclosingTrace”和“trace ”,这两个参数是什么关系呢?其实可以简单理解为“enclosingTrace”是“trace ”的父级堆栈,函数printEnclosedStackTrace中的while循环,就是为倒序找出“enclosingTrace”和“trace ”中从哪一个栈帧开始就不一样了,即“enclosingTrace”和“trace ”是有一部分是一样的(从数组后面倒回来),就是为了算出有多少个栈帧信息是重复可以隐藏的,相同的栈帧就不用重复输出了。

每个异常都输出一个完整的堆栈信息的话,都是从main函数开始,到当前的函数的所有函数调用的栈帧信息,里面函数的调用栈帧信息都会包括外层的函数调用栈帧信息,所以都输出的话,很多都是重复的,为了提高效率,减少io以及输出的内容太多又杂乱,所以jvm以“… n more”的方式隐藏了重复的部分。

当然,如果想不隐藏,可以重写java.lang.Throwable#printEnclosedStackTrace,去掉while部分,就可以看到每个异常的完整堆栈信息了,可以参考https://blog.csdn.net/michaelehome/article/details/79484722来验证。

Exception chaining:cause

工程中的最佳实践:捕捉并抛出一个更加贴合实际的Exception

原文:参考资料4、6

Let’s say we are working on a big project that deals with fictional FooBars, and our code is going to be used by others. We might decide to catch the ArithmeticException from Fraction and re-throw it as something project-specific, which looks like this:

1
2
3
4
5
6
7
try {
....
Fraction.getFraction(x,y);
....
} catch ( ArithmeticException e ){
throw new MyProjectFooBarException("The number of FooBars cannot be zero", e);
}

Catching the ArithmeticException and rethrowing it has a few benefits:

  • Our users are shielded from having to care about the ArithmeticException - giving us flexibility to change how commons-lang is used.
  • More context can be added, eg stating that it’s the number of FooBars that is causing the problem.
  • It can make stack traces easier to read, too, as we’ll see below.

It isn’t necessary to catch-and-rethrow on every Exception, but where there seems to be a jump in the layers of your code, like calling into a library, it often makes sense.

注意到MyProjectFooBarException的构造器有两个参数,第二个就是cause,用来指明产生此异常的根异常。

Every Exception in Java has a cause field, and when doing a catch-and-rethrow like this then you should always set that to help people debug errors.

设置了cause后stack trace变成了这样:

1
2
3
4
5
6
7
8
Exception in thread "main" com.myproject.module.MyProjectFooBarException: The number of FooBars cannot be zero
at com.myproject.module.MyProject.anotherMethod(MyProject.java:19)
at com.myproject.module.MyProject.someMethod(MyProject.java:12)
at com.myproject.module.MyProject.main(MyProject.java:8)
Caused by: java.lang.ArithmeticException: The denominator must not be zero
at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
at com.myproject.module.MyProject.anotherMethod(MyProject.java:17)
... 2 more

多了一行Caused by:和cause的栈信息。需要注意的是此时若要查看异常根源,需要找到最后的caused by后面的异常的第一个方法。

The “Caused by:” is only included in the output when the primary exception’s cause is not null). Exceptions can be chained indefinitely, and in that case the stacktrace can have multiple “Caused by:” traces.

SO, don’t handle exceptions in the intermediate layers, because code in the middle layers is often used by code in the higher layers. It’s responsibility of the code in the top-most layer to handle the exceptions. The top-most layer is typically the user interface such as command-line console, window or webpage. And typically we handle exceptions by showing a warning/error message to the user.

This good practice is illustrated by the following picture:

exception chaining rule

exception chaining rule

stack trace中类、方法名的表示的特殊情况

The class and method names in the stack frames are the internal names for the classes and methods. You will need to recognize the following unusual cases:

  • A nested or inner class will look like “OuterClass$InnerClass”.
  • An anonymous inner class will look like “OuterClass$1”, “OuterClass$2”, etcetera.
  • When code in a constructor, instance field initializer or an instance initializer block is being executed, the method name will be “”.
  • When code in a static field initializer or static initializer block is being executed, the method name will be “”.

将 stack trace 转为String

有时候我们需要将栈信息转换为String以便后面的操作,通常的做法是 create a temporary OutputStream or Writer that writes to an in-memory buffer and pass that to the printStackTrace(...).

1
2
3
4
5
6
7
8
9
10
11
/**
* Returns the string representation of the stack trace.
*
* @param throwable the throwable
* @return the string.
*/
public static String stackTraceToString(Throwable throwable) {
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString();
}

Apache CommonsGuava 都提供有进行这个操作的工具方法。

1
2
org.apache.commons.lang.exception.ExceptionUtils.getStackTrace(Throwable)
com.google.common.base.Throwables.getStackTraceAsString(Throwable)

留言

⬆︎TOP