JVM异常不打印堆栈信息 [ -XX:-OmitStackTraceInFastThrow ]

问题现象

	最近在排查线上日志的时候发现有个日志捕捉了NullPointerException,但是没有异常堆栈信息
	,只有java.lang.NullPointerException:null这一条信息,无法知道是从哪里抛出来的如图:

在这里插入图片描述

	经过分析是JIT编译器对异常进行了优化,当代码中的某个位置抛出同一个异常很多次后,JIT服务端编译器(C2)
	会将其优化成抛出一个事先编译好的、类型匹配的异常,异常堆栈信息就看不到了。

引用R大的一段话:

HotSpot VM有个许多人觉得“匪夷所思”的优化,叫做fast throw:有些特定的隐式异常类型(NullPointerException、ArithmeticException( / 0)之类)如果在代码里某个特定位置被抛出过多次的话,HotSpot Server Compiler(C2)会透明的决定用fast throw来优化这个抛出异常的地方——直接抛出一个事先分配好的、类型匹配的异常对象。这个对象的message和stack trace都被清空。抛出这个异常的速度是非常快,不但不用额外分配内存,而且也不用爬栈;但反面就是可能正好是需要知道哪里出问题的时候看不到stack trace了。从Sun JDK5开始要避免C2做这个优化还得额外传个VM参数:-XX:-OmitStackTraceInFastThrow

问题重现

既然知道这是JIT编译优化导致的,那就很容易写代码来重现,测试代码如下,使用的是JDK1.8.0_25:

public class Main {
    public static void main(String[] args) {
       for(int i = 0; i < 300000; i++) {
           try {
               ((Object)null).getClass();
           } catch (Exception e) {
               System.out.println(i + ":" + e.getStackTrace().length);
               if (e.getStackTrace().length == 0) {
                   System.out.println("stackTrace no stack after " + i + " times");
                   e.printStackTrace();
                   break;
               }
           }
       }

    }
}

测试结果是在调用115712次后JIT做了编译优化,在第115713次时异常堆栈看不到了,stackTrace长度为0:

115713:1
115714:1
115715:0
stackTrace omit after 115715 times
java.lang.NullPointerException

解决办法

JVM提供了 -XX:-OmitStackTraceInFastThrow这个虚拟机参数来告诉JIT编译器禁用这种异常fastThrow的优化,当然如果你使用-Xint参数后虚拟机运行在解释器模式也不会出现这个问题,但是禁用JIT会对整体的性能有影响,因此不建议使用-Xint参数,如果想看到具体的异常堆栈,推荐使用 -XX:-OmitStackTraceInFastThrow 参数。

那JVM为什么要对异常进行优化呢,这里就牵扯到另一个问题了,如果你在系统响应慢的时候分析过线程堆栈,可能遇到过线程耗在调用fillInStackTrace()这个native方法的时间非常长,fillInStackTrace()方法用来爬取线程的调用堆栈,我之前就遇到过Log4j打印日志非常慢的问题,结果抓取线程堆栈后发现线程都是卡在fillInStackTrace()这个native方法,如果有些使用场景不需要完整的调用堆栈时,建议重写fillInStackTrace(),让它直接return this,可以一定程度的提高系统的吞吐量。

fillInStackTrace优化
我们知道所有的Exception和Error都是Throwable的子类,构造子类实例前都先调用父类的实例构造方法,我们看下Throwable类的源码就会发现在构造方法里调用了fillInStackTrace()方法:

/**
 * Constructs a new throwable with {@code null} as its detail message.
 * The cause is not initialized, and may subsequently be initialized by a
 * call to {@link #initCause}.
 *
 * <p>The {@link #fillInStackTrace()} method is called to initialize
 * the stack trace data in the newly created throwable.
 */
public Throwable() {
    fillInStackTrace();
}

public synchronized Throwable fillInStackTrace() {
    if (stackTrace != null || backtrace != null ) {
		//这里调用native的fillInStackTrace方法
        fillInStackTrace(0);
        stackTrace = UNASSIGNED_STACK;
    }
    return this;
}

可以看到当stackTrace不为null时需要调用native的fillInStackTrace()方法,那什么时候stackTrace变量为null呢,通过追踪源码可以发现Throwable类有多个重载的构造方法,其中有个方法可以传递一个writableStackTrace参数,当这个参数为false的时候stackTrace就为null,这时候就不会调用native的fillInStackTrace()方法去爬取线程堆栈,当然你也可以重写fillInStackTrace()方法,让他直接返回this,这样也可以避免爬栈,但是还是建议大家谨慎使用,毕竟需求时刻在变,说不定什么时候就需要这个堆栈来定位问题了。

protected Throwable(String message, Throwable cause,
                    boolean enableSuppression,
                    boolean writableStackTrace) {
    if (writableStackTrace) {
        fillInStackTrace();
    } else {
        stackTrace = null;
    }
    detailMessage = message;
    this.cause = cause;
    if (!enableSuppression)
        suppressedExceptions = null;
}

前面提到了Log4j打印日志慢的问题,那Log4j打印日志为什么也涉及到这个fillInStackTrace方法呢,对Log4j有研究过的同学应该知道如果Log4j配置文件里配置了%C(类全限定包名)、%F(文件名)、%M(打印日志的方法名称)和%L(行号)这几个用于定位调用者信息的pattern时,Log4J会先抛出一个异常出来,然后从异常堆栈中来获取调用者的信息,既然是抛异常出来必然涉及到调用native的fillInStackTrace方法来爬取线程堆栈,因此开启这些参数对系统的性能是有影响的。

官方文档

文档地址:https://www.oracle.com/java/technologies/javase/release-notes-introduction.html#vm
内容:
在这里插入图片描述

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一个使用Java语言编写的应用程序的命令行运行指令。该程序是一个消息队列中间件的Broker,使用了RocketMQ框架实现。其中的参数含义如下: -server:使用JVM的server模式。在多核CPU上提高性能。 -Xms2g:JVM启动时堆内存的最小值为2G。 -Xmx2g:JVM堆内存的最大值为2G。 -XX:+UseG1GC:使用G1垃圾回收器。 -XX:G1HeapRegionSize=16m:G1垃圾回收器内存区域的大小为16M。 -XX:G1ReservePercent=25:预留25%的空间以避免太满引发的性能问题。 -XX:InitiatingHeapOccupancyPercent=30:G1在堆内存使用达到30%时会触发垃圾回收。 -XX:SoftRefLRUPolicyMSPerMB=0:清除软引用的时间间隔为0,即软引用的对象一旦没有被使用就会被立即清除。 -verbose:gc:打印GC日志。 -Xloggc:/dev/shm/rmq_srv_gc_%p_%t.log:将GC日志输出到/dev/shm/rmq_srv_gc_%p_%t.log文件中。 -XX:+PrintGCDetails:打印GC详细信息。 -XX:+PrintGCDateStamps:打印GC时间戳。 -XX:+PrintGCApplicationStoppedTime:打印应用程序停止时间。 -XX:+PrintAdaptiveSizePolicy:打印自适应策略的信息。 -XX:+UseGCLogFileRotation:启用GC日志文件轮换。 -XX:NumberOfGCLogFiles=5:GC日志文件轮换时保留的文件数目。 -XX:GCLogFileSize=30m:GC日志文件的大小为30M。 -XX:-OmitStackTraceInFastThrow:关闭快速抛出异常时的栈信息。 -XX:+AlwaysPreTouch:在JVM启动时预先分配堆内存。 -XX:MaxDirectMemorySize=15g:最大直接内存大小为15G。 -XX:-UseLargePages:不使用大页面。 -XX:-UseBiasedLocking:不使用偏向锁。 -Drocketmq.client.logUseSlf4j=true:使用SLF4J作为日志框架。 -c ../conf/broker.conf:指定Broker的配置文件路径。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值