线上异常处理多了,应该都会遇到线上日志有打印异常, 但无堆栈信息的情况。
比如这样,就一个null在日志里,没有对应的堆栈信息,让人一头雾水。
跑下demo:
输出如下,在正常打印了一段异常堆栈后,打印信息就只剩下一个异常类型了,堆栈信息“不见了”。

原因
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.
翻译一下就是
在服务器VM(Server VM)中,编译器现在可以为所有“冷”的内置异常提供正确的堆栈回溯信息。为了提高性能,当这种异常被抛出几次后,方法可能会被重新编译。重新编译后,编译器可能选择一种更快的策略,使用预分配的异常对象,这些对象不会提供堆栈跟踪。如果你希望完全禁用使用预分配异常对象,可以使用新的标志-XX:-OmitStackTraceInFastThrow。
简单点说就是JVM对于一些内置异常,为了提高性能,会在抛出多次后进行重新编辑优化,不再提供堆栈信息。
哪些内置异常?
那么哪些内置异常会触发这个堆栈优化呢?
可以在JVM源码中找到这个答案。
在【graphKit.cpp】中,搜索“
OmitStackTraceInFastThrow”,找到对应代码。所以,针对如下类型的异常,JVM都会优化。
在Windows和MacOS上实测只有以下异常会优化输出。

代码如下
如何避免优化堆栈打印
从官方的介绍中,可以知道在JVM参数中添加
-XX:-OmitStackTraceInFastThrow 即可避免对内置异常的堆栈优化。

一样的代码,for循环一样的次数,可以看到都是有堆栈信息的。
当然,在实际生产环境上,一般不轻易修改默认的JVM参数,这种情况下,只要日志记录的时间够长,发现问题时还能找到最开始打印出来的堆栈信息,那也是可以快速定位问题的。日志已经丢失的情况下,重启下服务节点,查看最新的打印堆栈也是可行的。
其他
在官方的说明中 “当这种异常被抛出几次后,方法可能会被重新编译。重新编译后,编译器可能选择一种更快的策略” 提到重新编译后,才可能选择更快的策略进行快速异常优化。
那么,不让它重新编译优化是不是也可以一直打印堆栈信息呢?
在这之前需要了解JIT和C1、C2编译。
JIT 编译器
JIT 编译器是 JVM 的一部分,用于在程序运行时将 Java 字节码编译为本地机器码。这样做的目的是提高应用程序的执行速度,因为本地机器码的执行速度比解释执行字节码要快得多。
JIT 编译器在程序运行过程中动态地分析哪些部分的代码被频繁执行(也称为热点代码),并将这些代码编译为高效的本地机器码。编译后的本地机器码直接在硬件上执行,从而大大提高了程序的性能。
C1 编译器
C1 编译器,又称客户端编译器或简单编译器,是 JVM 中的一种 JIT 编译器,主要针对启动时间要求较高且运行时性能要求相对较低的应用程序。
特点
- 快速编译:C1 编译器的目标是快速生成机器码,因此编译时间较短。
- 适度优化:虽然优化程度较低,但足以显著提升执行性能。适用于对启动速度要求高的桌面应用程序或开发环境。
C2 编译器
C2 编译器,又称服务器编译器或优化编译器,是 JVM 中的另一种 JIT 编译器,主要针对运行时性能要求高的应用程序。
特点
- 高级优化:C2 编译器会进行深入的优化,例如方法内联、循环展开、逃逸分析等,以生成高效的机器码。
- 编译速度较慢:由于进行的优化较多,C2 编译器的编译速度比 C1 编译器慢,但生成的机器码执行效率更高。适用于服务器端应用或长时间运行的程序。
JVM 的分层编译策略
JVM 通常使用分层编译(Tiered Compilation)策略,结合使用 C1 和 C2 编译器,以兼顾启动速度和运行时性能。
分层编译的工作方式
- 解释执行:程序启动时,JVM 使用解释器逐行解释执行字节码。
- C1 编译:对于频繁执行的代码,JVM 会首先使用 C1 编译器进行编译,生成中等优化的机器码,以便快速提高性能。
- C2 编译:对于特别热点的代码,JVM 会进一步使用 C2 编译器进行编译,生成高度优化的机器码,最大化运行时性能。
在Java8中,分层编译通常是默认启用的,但可以通过以下 JVM 参数显式配置:
其他编译策略
- 仅使用 C1 编译器:可以通过以下参数配置 JVM 仅使用 C1 编译器:
- 仅使用 C2 编译器:可以通过以下参数配置 JVM 仅使用 C2 编译器:
仅使用C1编译器的情况下,异常的堆栈信息是不会被优化掉的。


仅使用C2编译器的情况下,异常的堆栈信息会被优化掉。


注:文中提到的JVM默认指HotSpot VM实现。Windows或MacOS环境如下


📎 参考文章
- 作者:Yibin
- 链接:https://yibin.dev/article/ff73b648-f799-45b2-9994-66727287f3ab
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章





