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

原因

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上实测只有以下异常会优化输出。
notion image
代码如下

如何避免优化堆栈打印

从官方的介绍中,可以知道在JVM参数中添加-XX:-OmitStackTraceInFastThrow 即可避免对内置异常的堆栈优化。
notion image
notion image
一样的代码,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 编译器,以兼顾启动速度和运行时性能。

分层编译的工作方式

  1. 解释执行:程序启动时,JVM 使用解释器逐行解释执行字节码。
  1. C1 编译:对于频繁执行的代码,JVM 会首先使用 C1 编译器进行编译,生成中等优化的机器码,以便快速提高性能。
  1. C2 编译:对于特别热点的代码,JVM 会进一步使用 C2 编译器进行编译,生成高度优化的机器码,最大化运行时性能。
 
在Java8中,分层编译通常是默认启用的,但可以通过以下 JVM 参数显式配置:
其他编译策略
  • 仅使用 C1 编译器:可以通过以下参数配置 JVM 仅使用 C1 编译器:
    • 仅使用 C2 编译器:可以通过以下参数配置 JVM 仅使用 C2 编译器:
       
      仅使用C1编译器的情况下,异常的堆栈信息是不会被优化掉的。
      notion image
      notion image
       
      仅使用C2编译器的情况下,异常的堆栈信息会被优化掉。
      notion image
      notion image
       
      注:文中提到的JVM默认指HotSpot VM实现。Windows或MacOS环境如下
      notion image
      notion image

      📎 参考文章

       
      PVE 8.x 折腾WiFi二维码登录流程解析
      Loading...