抛异常的最大开销是异常栈的构建过程,如果你的程序调用很深,特别是用了第三方开源框架,这个开销是不容忽视的
开销在哪
查看jdk源码
/**
* Constructs a new throwable with the specified cause and a detail
* message of {@code (cause==null ? null : cause.toString())} (which
* typically contains the class and detail message of {@code cause}).
* This constructor is useful for throwables that are little more than
* wrappers for other throwables (for example, {@link
* java.security.PrivilegedActionException}).
*
* <p>The {@link #fillInStackTrace()} method is called to initialize
* the stack trace data in the newly created throwable.
*
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A {@code null} value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public Throwable(Throwable cause) {
fillInStackTrace();
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}
主要的性能瓶颈在fillInStackTrace,这是一个native方法.会构建整个异常栈. 方法签名如下,有synchronized修饰的同步方法,对性能影响较大
/**
* Fills in the execution stack trace. This method records within this
* {@code Throwable} object information about the current state of
* the stack frames for the current thread.
*
* <p>If the stack trace of this {@code Throwable} {@linkplain
* Throwable#Throwable(String, Throwable, boolean, boolean) is not
* writable}, calling this method has no effect.
*
* @return a reference to this {@code Throwable} instance.
* @see java.lang.Throwable#printStackTrace()
*/
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
fillInStackTrace(0);
stackTrace = UNASSIGNED_STACK;
}
return this;
}
private native Throwable fillInStackTrace(int dummy);
如何解决
- 创建异常类的时候重写fillInStackTrace方法,java7已原生支持:新增了个是否执行fillInStackTrace方法的开关
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;
}
- 去掉异常.现在很多业务系统用异常实现程序正常业务逻辑.这个对性能影响比较大,尤其是并发比较大的时候.
寻找异常
有时候你无法知道那个异常抛的最多,有些三方包 自己throw Exception 但自己又catch住.
- brace 跟踪Exception 对异常栈,汇总
- perf top去看下us的开销,如果_ZN19java_lang_Throwable19fill_in_stack_traceE6HandleP6Thread这个排名很靠前,那就好好检查下
讨论
用异常实现正常的业务流程有以下优点
- 代码比较精炼.增强代码可读性.可以不用对服务方法设计统一的返回值
- 可以使用切面技术(拦截器 异常处理器) 对异常做统一的监控 和处理.
缺点:性能
改进:
- 不构建异常堆栈,而是保存失败原因FailCause或者错误码
- 重写fillInStackTrace方法进行noop
知乎上一篇讨论:
作者:RednaxelaFX
链接:https://www.zhihu.com/question/21405047/answer/45055055
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
只要那个自定义异常类型是真的不需要stack trace的,我也会推荐覆写fillInStackTrace()为直接返回this。爬栈是抛异常开销大的主要原因之一。<- 但注意好前提就是了——要确定真的肯定绝对不需要stack trace的话。一个简单的例子是,“滥用”异常来实现某些特殊控制流结构的场景,此时stack trace肯定是没用的,那个异常对象本身其实也没用,只有它的类型和抛出它带来的控制流跳转才有用,那就应该覆写fillInStackTrace()。=======================================然后从反面举个例子。HotSpot VM有个许多人觉得“匪夷所思”的优化,叫做fast throw:有些特定的隐式异常类型(NullPointerException、ArithmeticException( / 0)之类)如果在代码里某个特定位置被抛出过多次的话,HotSpot Server Compiler(C2)会透明的决定用fast throw来优化这个抛出异常的地方——直接抛出一个事先分配好的、类型匹配的异常对象。这个对象的message和stack trace都被清空。抛出这个异常的速度是非常快,不但不用额外分配内存,而且也不用爬栈;但反面就是可能正好是需要知道哪里出问题的时候看不到stack trace了。从Sun JDK5开始要避免C2做这个优化还得额外传个VM参数:-XX:-OmitStackTraceInFastThrow。覆写fillInStackTrace()为直接返回this就像是人肉做C2所做的那种优化…的一部分效果。反正肯定会有人抱怨这样不好的啦,是不是要顶住压力硬上就看到底在特定场景里带来的性能好处是不是真的那么重要了。
关于上面说的“匪夷所思”的优化:“fast throw”,我在日常工作查看错误日志的时候经常看到,某个代码位置频繁抛出NullPointerException,一开始还会打印出异常堆栈,但是后来就只会 打印出 “throw NullPointerException”这么一句话,通过加 -XX:-OmitStackTraceInFastThrow启动参数解决了这个问题