OmitStackTraceInFastThrow与JVM的C1,C2编译器

2,000 阅读5分钟

现象

java.lang.ArrayIndexOutOfBoundsException: null

有个同事发现生产上的异常打印没有堆栈,像上面那样,只有异常没有堆栈,不方便排查问题,一开始我以为是用System.out打印出来,最终终于发现是OmitStackTraceInFastThrow所引起的

重现代码如下:

 public static void main(String[] args) {
        for (int i = 0; i < 1000 * 1000; i++) {
            try {
                t();
            } catch (Exception e) {
                if (null != e.getStackTrace() && e.getStackTrace().length <= 0) {
                    LOGGER.info("exception {}, {}", i, e);
                    return;
                }
            }
        }
    }
    public static void t() {
        int[] nums = new int[1];
        int m = nums[4];
    }

OmitStackTraceInFastThrow

这是HotSpot VM专门针对异常做的一个优化,默认启用,当一些异常在代码里某个特定位置被抛出很多次的话,HotSpot Server Compiler(C2)会用fast throw来优化这个抛出异常的地方,直接抛出一个事先分配好的,类型匹配的对象,这个对象的message和stack trace都被清空.

以上是网上关于OmitStackTraceInFastThrow的解释,信息量非常大,我们逐条分析.

  1. 只针对HotSpot VM才有, 例如oracleJDK, libericaJDK等
  2. 特定位置抛出很多次,其实就是JIT将它优化了
  3. JIT必须使用C2才会这样优化,不抛出原来的异常,改用fast throw抛出
  4. 这是一个事先分配好的异常,message和堆栈都是空的 可以看出,如果某个异常在同一位置被抛出多次,会被JIT C2优化成空异常,例如本文的ArrayIndexOutOfBoundsException,既没有message,也没有堆栈.但他的速度非常快,不用分配内存和获取堆栈.

缺点也是有的,需要知道哪里出问题的时候看不到堆栈了,不利于排查问题.(当然了如果日志有保存好方便检索,也能通过前面的日志找到堆栈)

如果想关闭这个优化,设置-XX:-OmitStackTraceInFastThrow即可

C1 C2编译器

上面提到的C2编译器是什么? 这里摘抄一篇文章的段落www.cnblogs.com/death00/p/1…

JIT

当 JVM 的初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译JIT

最初,JVM 中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为热点代码

为了提高热点代码的执行效率,在运行时,即时编译器(JIT,Just In Time)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。

C1编译器

C1编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,也称为Client Compiler,例如,GUI 应用对界面启动速度就有一定要求。

C2编译器

C2编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序,也称为Server Compiler,例如,服务器上长期运行的 Java 应用对稳定运行就有一定的要求。

分层编译

在 Java7 之前,需要根据程序的特性来选择对应的 JIT,虚拟机默认采用解释器和其中一个编译器配合工作。

Java7 引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,我们也可以通过参数 -client或者-server 强制指定虚拟机的即时编译模式。

分层编译将 JVM 的执行状态分为了 5 个层次:

  • 第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译;
  • 第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;
  • 第 2 层:也称为 C1 编译,开启 Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译;
  • 第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;
  • 第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

对于 C1 的三种状态,按执行效率从高至低:第 1 层、第 2层、第 3层。

通常情况下,C2 的执行效率比 C1 高出30%以上。

在 Java8 中,默认开启分层编译,-client-server 的设置已经是无效的了。如果只想开启 C2,可以关闭分层编译-XX:-TieredCompilation,如果只想用 C1,可以在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1

JVM参数

为了观察以上提到的值,我们需要通过JVM参数确认是否生效,下面提供三个常见参数

  1. -XX:+PrintFlagsInitial: 这个参数显示在处理参数之前所有可设置的参数及它们的值,然后直接退出程序
  2. -XX:+PrintCommandLineFlags: 这个参数的作用是显示出VM初始化完毕后所有跟最初的默认值不同的参数及它们的值
  3. -XX:+PrintFlagsFinal: 显示JVM初始化完后的参数值

验证

验证用的JDK是libericaJDK

image.png

java -Xint -jar xxx.jar

以解释器Interpreter模式运行(关闭C1, C2),相当于-XX:-UseCompiler,不能重现问题

java -XX:TieredStopAtLevel=1 -jar xxx.jar

只使用C1编译器,并停留在第一层,不能重现问题

特别需要注意的是, IDEA使用SpringBoot项目时,默认勾选Enable launch optimization,优化启动速度,同样导致不能重现问题 image.png

java -XX:TieredStopAtLevel=4 -jar xxx.jar

使用C1,C2编译器,并停留在第四层,这也是默认值,在110000次前后稳定重现问题

java -XX:-TieredCompilation -jar xxx.jar

关闭分层编译(即只有C2), 在循环10000次前后稳定重现,修改-XX:CompileThreshold=200000, 在循环20000次前后稳定重现

java -XX:-OmitStackTraceInFastThrow -jar xxx.jar

关闭fast throw优化,无论如何都不会重现.

总结

OmitStackTraceInFastThrow生效必须具备两个条件

  1. OmitStackTraceInFastThrow=true
  2. C2编译器生效

如果在生产中遇到此类情况,尽量先查找过往的日志锁定堆栈,后面投产也可以关闭OmitStackTraceInFastThrow