现象
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的解释,信息量非常大,我们逐条分析.
- 只针对HotSpot VM才有, 例如oracleJDK, libericaJDK等
- 特定位置抛出很多次,其实就是JIT将它优化了
- JIT必须使用C2才会这样优化,不抛出原来的异常,改用fast throw抛出
- 这是一个事先分配好的异常,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参数确认是否生效,下面提供三个常见参数
-XX:+PrintFlagsInitial
: 这个参数显示在处理参数之前所有可设置的参数及它们的值,然后直接退出程序-XX:+PrintCommandLineFlags
: 这个参数的作用是显示出VM初始化完毕后所有跟最初的默认值不同的参数及它们的值-XX:+PrintFlagsFinal
: 显示JVM初始化完后的参数值
验证
验证用的JDK是libericaJDK
java -Xint -jar xxx.jar
以解释器Interpreter
模式运行(关闭C1, C2),相当于-XX:-UseCompiler
,不能重现问题
java -XX:TieredStopAtLevel=1 -jar xxx.jar
只使用C1编译器,并停留在第一层,不能重现问题
特别需要注意的是, IDEA使用SpringBoot项目时,默认勾选Enable launch optimization
,优化启动速度,同样导致不能重现问题
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
生效必须具备两个条件
OmitStackTraceInFastThrow
=trueC2编译器
生效
如果在生产中遇到此类情况,尽量先查找过往的日志锁定堆栈,后面投产也可以关闭OmitStackTraceInFastThrow