今天有一台服务器频繁打印 java.lang.NullPointerException
,但是不包含栈帧信息,起初我以为是应用层代码使用 log4j2 打印时直接调用了 log.error(e)
进行打印,因为该方法最终会调用异常对象的 toString()
方法然后打印出异常信息,对于 java.lang.NullPointerException
实例,最终调用的 java.lang.Throwable#toString
获取到需要打印的字符串:
1 | /** |
即会打印出不含栈帧的异常信息,如:java.lang.NullPointerException
。一般来说,我们尽量避免调用 log.error(e)
而是调用 log.error("some message", e)
以使打印出的字符串含有栈帧。
但是在今天这个场景下,经过我的检查,确认 java.lang.NullPointerException
不是由 log.error(e)
打印出,那是哪里打印出的呢?且因为没有栈帧信息,导致难以定位异常根源,只能看到 java.lang.NullPointerException
这样简短的信息,经过一阵排查,最后确认是 JVM 优化导致,即对于部分异常类型,如果抛出达到一定的次数,JVM 将会使用预构建的异常实例代替实际需要抛出的异常实例进行抛出,以提升性能,但是预构建的异常实例是不含栈帧信息的,这就导致打印出来的异常信息不含栈帧了,增大了问题排查的难度。关于该项优化,在 Release Notes 中有以下说明:
The compiler in the server VM now provides correct stack backtraces for all “cold” built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag:
-XX:-OmitStackTraceInFastThrow
.
JVM 中关于这部分优化的源码位于 graphKit.cpp at jdk8-b120:
1 | // If this throw happens frequently, an uncommon trap might cause |
可以编写简短的代码来复现该项优化,比如如下代码:
1 | import java.util.concurrent.ThreadLocalRandom; |
我们使用命令行编译并执行以上代码可以观察到部分输出如下:
1 | java.lang.NullPointerException |
即打印一定次数后打印出的数据就不含栈帧信息了,我们微调上面的代码,改为打印异常对象的 System.identityHashCode(e)
:
1 | import java.util.concurrent.ThreadLocalRandom; |
观察到部分输出如下:
1 | 1597422095 |
即验证了上述的代码,执行到一定次数后,JVM 会将异常替换为预构建的异常,导致打印出的异常信息中不含栈帧,确认该问题原因后,我在 JVM 配置选项中加上了 -XX:-OmitStackTraceInFastThrow
以避免 JVM 执行此项优化。
Reference
What Is JVM Option “-XX:-OmitStackTraceInFastThrow” ? | YoshiTech Blog
How is the count of method executions that triggers the omitted exception stack frames calculated in JDK 8? - Stack Overflow
jdk/advancedThresholdPolicy.hpp at jdk8-b120 · openjdk/jdk · GitHub
How does the JVM decided to JIT-compile a method (categorize a method as “hot”)? - Stack Overflow
Tiered Compilation in JVM | Baeldung