今天登陆服务器进行例行的检查,发现异常日志文件里有很多nullPointException,只有简单的异常名称,却没有堆栈信息。没有异常堆栈,无法定位错误,也就不能修改了。到网上搜索信息,原来大家也遇到过这样的问题。
正确的解决方法是增加一个VM Options:-XX:-OmitStackTraceInFastThrow。这个参数的好处如下:
“JVM对一些特定的异常类型做了Fast Throw优化,如果检测到在代码里某个位置连续多次抛出同一类型异常的话,C2会决定用Fast Throw方式来抛出异常,而异常Trace即详细的异常栈信息会被清空。这种异常抛出速度非常快,因为不需要在堆里分配内存,也不需要构造完整的异常栈信息。”
这个参数,支持的异常类型如下:
翻阅了大量的关于此问题的文章,介绍信息大同小异。通过这个方案,开启输出空指针错误,很快就定位问题,并解决了。
解决问题后,进行了一番的测试和验证,如下:
众多网络文章,都指出在异常出现5000次以上时,才会丢失堆栈信息。按照网络文章的测试代码,在6600多次的时候丢失堆栈信息,但是并不稳定。代码如下:
public class JAVANPE extends Thread {
private static int count = 0;
@Override
public void run() {
try {
System.out.println("getSimpleName is:"+this.getClass().getSimpleName() + " execute count:" + (++count));
String str = null;
System.out.println(str.length());
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public class TestFastThrow {
public static void main(String[] args) throws InterruptedException {
JavaNPE javaNPE = new JavaNPE();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executorService.execute(javaNPE);
//防止打出的日志太快
Thread.sleep(2);
}
}
}
这是一个多线程的程序,写单线程的测试程序,是否可行呢?于是就尝试了第一版,代码如下:
public static void main(String args[]) {
for (int i = 0; i < 10000; i++) {
try {
String value = null;
value.substring(0, 100);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
但是,很遗憾,这个程序的异常堆栈信息并不会丢失。程序修改如下:
public void method2() {
String value = null;
value.substring(0, 100);
}
public static void main(String args[]) {
ExceptionTest exceptionTest = new ExceptionTest(1);
for (int i = 0; i < 10000; i++) {
try {
exceptionTest.method2();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
这个程序在第5530~5550次的时候,会丢失异常堆栈信息,是不稳定的。分析上述两段单线程的测试代码,第一段因为异常信息没有到方法的外面,jvm不能追踪到异常堆栈信息,所以并不会起作用。
首先,在网络上的文章,都着重说C2编译器的优化,并没有说明C1编译器。通过我的实验,C1和C2编译器的默认行为是一样的,都是开启OmitStackTraceInFastThrow的。C1和C2编译器,是指Java及时编译技术(JIT),在启动应用程序是,java -client是启动C1编译器;java -server是启动C2编译器。
这个参数真的有性能优化吗?测试代码没有IO输出,纯测试代码的执行速度,测试代码如下:
public void testPerformance() {
long start = System.currentTimeMillis();
System.out.println("start");
for (int i = 0; i < 1000 * 1000; i++) {
try {
this.method();
} catch (Exception ex) {
}
}
System.out.println("used " + (System.currentTimeMillis() - start));
}
public void method() {
String value = null;
value.substring(0, 100);
}
测试结果如下:
测试结果发现,关闭堆栈后性能确实有了很大的提升,在输出堆栈的情况下,性能随着调用次数的增多,呈线性增长。
测试代码中的堆栈信息,只有一层,把调用异常堆栈的深度增加到20,是不是创建的对象而更多,耗时时间更长呢?关闭参数的情况下,确实是,调用1M次,时间增加到由13秒增加到20秒;启用参数的情况下,耗时却从400毫秒减少到150毫秒!
经测试发现:同样都是在5500多次的时候,丢掉异常的堆栈信息。并且20层异常堆栈,执行这5500多次,耗时120毫秒;层数较少的只用了70毫秒。也就是说,堆栈层数越多,丢掉堆栈后的性能越好!具体是什么原因,就需要阅读源代码了!如果你知道,欢迎留言探讨。