一、问题
排查线上问题时,发现日志中异常输出的地方,仅有一行java.lang.NullPointerException: null
,截图如下。
丢失了具体的异常栈,导致无法定位是哪行代码抛出了异常。
这里排除日志用法的问题,以前是正常能输出异常栈的。
二、原因
HotSpot VM有个许多人觉得“匪夷所思”的优化,叫做fast throw
:有些特定的隐式异常类型(NullPointerException、ArithmeticException( / 0)之类)如果在代码里某个特定位置被抛出过多次的话
,HotSpot Server Compiler(C2)会透明的决定用fast throw来优化这个抛出异常的地方——直接抛出一个事先分配好的、类型匹配的异常对象
。这个对象的message和stack trace都被清空;抛出这个异常的速度就会非常快,不但不用额外分配内存,而且也不用爬栈。但反面就是可能正好是需要知道哪里出问题的时候看不到stack trace了。从Sun JDK5开始要避免C2做这个优化,需要用户额外传个VM参数:-XX:-OmitStackTraceInFastThrow
。
出处:https://www.oracle.com/java/technologies/javase/release-notes-introduction.html#vm
java.lang.NullPointerException 报错115715次左右后,不再打印异常栈
java.lang.ArithmeticException 报错41984次左右后,不再打印异常栈
三、复现&解决
1. 复现
1.1 java.lang.ArithmeticException复现
public static void main(String[] args) {
for(int i = 0; i < 300000; i++) {
try {
System.out.println(1/0);
} catch (Exception e) {
System.out.println("已报错" + i + "次!");
if (e.getStackTrace().length == 0) {
System.out.println("异常栈追踪停止,报错次数为:" + i);
e.getStackTrace();
break;
}
}
}
}
循环报错30W次,ArithmeticException异常
在报错41984次
左右后,不再进行打印异常栈。
1.2 java.lang.NullPointerException 复现
这个异常不能直接使用idea运行复现
,需要使用基础的Java命令执行才可以。
代码如下:
public static void main(String[] args) {
for(int i = 0; i < 300000; i++) {
try {
((Object)null).getClass();
} catch (Exception e) {
System.out.println("已报错" + i + "次!");
if (e.getStackTrace().length == 0) {
e.printStackTrace();
System.out.println("异常栈追踪停止,报错次数为:" + i);
break;
}
}
}
}
运行
## 进入到classes文件根目录
cd ~/IdeaProjects/my_project/jtest/jdk/target/classes
## 执行classes类
java com.kaka.jtest.jdk.jvm.other.FastThrow
循环报错30W次,NullPointerException异常
在报错115715次
左右后,不再进行打印异常栈。
2. 解决
使用idea运行前增加-XX:-OmitStackTraceInFastThrow
参数即可。
ArithmeticException异常
正常报错30W次,没有再被优化。
四、随便聊聊
其实这个问题比较难遇到,但当遇到时一定要知道其背后的原因。经验 = 踩过的坑 + 避坑办法
,随着经验的不断累积,会潜移默化的影响你解决问题的思路。
第一次运行classes的姿势不对(直接在classes文件目录中,使用了Java命令),能快速调整正确,其实就是多年前曾踩过类似的坑。
运行classes文件
使用Java命令运行一个classes文件时,需要在该主类的全路径,对应根目录执行。因为Java以执行目录为基础,根据全类名去一层层的去找classes文件。