谨慎使用Exception
- "返回异常码”:在业务代码中return错误码
- “抛出异常+捕获转为返回异常码”:有种观点认为,业务失败异常流程应该基于Exception控制,在这样的项目里就会看到大量的基于业务定义的Exception类,比如UserNotFoundException,LoginFailException什么的。或者把Service层所有的异常分支都包装成一个ServiceException什么的。
package com.dxz.statement; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Benchmark) public class TryCatchTest { private int status; public void init() { status = 0; } @Benchmark public boolean catchException() { try { business(status); return true; } catch (Exception ex) { return false; } } @Benchmark public boolean errorCode() { int retCode = businessWitErrorCode(status); return retCode == 1; } protected void business(int input) { if(input == 0) { throw new IllegalArgumentException("模拟业务抛出异常"); } //模拟正常业务 return ; } protected int businessWitErrorCode(int input) { if (input == 0) { return 0; } return 1; } public static void main(String[] args) { Options opt = new OptionsBuilder().include(TryCatchTest.class.getSimpleName()) .forks(2).build(); try { new Runner(opt).run(); } catch (RunnerException e) { e.printStackTrace(); } } }
结果:
status=0时:
Benchmark Mode Cnt Score Error Units TryCatchTest.catchException thrpt 10 0.918 ± 0.012 ops/us TryCatchTest.errorCode thrpt 10 399.346 ± 2.210 ops/us
status=1时:
Benchmark Mode Cnt Score Error Units TryCatchTest.catchException thrpt 10 0.913 ± 0.006 ops/us TryCatchTest.errorCode thrpt 10 396.107 ± 1.579 ops/us
通过JMH结果可以看出性能高出很多,因此我们应该避免把正常的返回错误结果使用异常来代替。
一、抛出异常之所以导致性能降低的原因
原因是:创建异常对象时会调用父类Throwable的fillInStackTrace()方法生成栈追踪信息,也就是调用native的fillInStackTrace()方法去爬取线程堆栈信息,为运行时栈做一份快照,正是这一部分开销很大。
涉及到的源码在Throwable类中,有两个方法如下:
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是一个Native方法,会填写异常栈。可想而知,这是一个异常耗时的操作,优化方法是自定义一个异常,重载fillInStackTrace方法,不执行fillInStackTrace操作。
二、优化方法
2.1、重载fillInStackTrace方法,不执行fillInStackTrace操作
package com.dxz.statement; public class LightException extends RuntimeException { public LightException(String msg) { super(msg); } public synchronized Throwable fillInStackTrace() { this.setStackTrace(new StackTraceElement[0]); return this; } }
修改示例1中的demo,使用LightException替换IllegalArgumentException,性能有了明显改善,提高了两个数量级。
Benchmark Mode Cnt Score Error Units TryCatchTest.catchException thrpt 10 45.526 ± 0.925 ops/us TryCatchTest.errorCode thrpt 10 395.455 ± 13.376 ops/us
package com.dxz.statement; public class LightExceptionTest { public void business() { if(1 == 1) { throw new LightException("测试异常信息"); } } public static void main(String[] args) { LightExceptionTest let = new LightExceptionTest(); let.business(); } }
结果:
C:\java\jdk1.8.0_111\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.2\lib\idea_rt.jar=51627:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.2\bin" -Dfile.encoding=UTF-8 -classpath C:\java\jdk1.8.0_111\jre\lib\charsets.jar;C:\java\jdk1.8.0_111\jre\lib\deploy.jar;C:\java\jdk1.8.0_111\jre\lib\ext\access-bridge-64.jar;C:\java\jdk1.8.0_111\jre\lib\ext\cldrdata.jar;C:\java\jdk1.8.0_111\jre\lib\ext\dnsns.jar;C:\java\jdk1.8.0_111\jre\lib\ext\jaccess.jar;C:\java\jdk1.8.0_111\jre\lib\ext\jfxrt.jar;C:\java\jdk1.8.0_111\jre\lib\ext\localedata.jar;C:\java\jdk1.8.0_111\jre\lib\ext\nashorn.jar;C:\java\jdk1.8.0_111\jre\lib\ext\sunec.jar;C:\java\jdk1.8.0_111\jre\lib\ext\sunjce_provider.jar;C:\java\jdk1.8.0_111\jre\lib\ext\sunmscapi.jar;C:\java\jdk1.8.0_111\jre\lib\ext\sunpkcs11.jar;C:\java\jdk1.8.0_111\jre\lib\ext\zipfs.jar;C:\java\jdk1.8.0_111\jre\lib\javaws.jar;C:\java\jdk1.8.0_111\jre\lib\jce.jar;C:\java\jdk1.8.0_111\jre\lib\jfr.jar;C:\java\jdk1.8.0_111\jre\lib\jfxswt.jar;C:\java\jdk1.8.0_111\jre\lib\jsse.jar;C:\java\jdk1.8.0_111\jre\lib\management-agent.jar;C:\java\jdk1.8.0_111\jre\lib\plugin.jar;C:\java\jdk1.8.0_111\jre\lib\resources.jar;C:\java\jdk1.8.0_111\jre\lib\rt.jar;D:\study\jmh\benchmark-demo\target\classes;C:\Users\4cv748wpd3\.m2\repository\org\openjdk\jmh\jmh-core\1.25\jmh-core-1.25.jar;C:\Users\4cv748wpd3\.m2\repository\net\sf\jopt-simple\jopt-simple\4.6\jopt-simple-4.6.jar;C:\Users\4cv748wpd3\.m2\repository\org\apache\commons\commons-math3\3.2\commons-math3-3.2.jar;C:\Users\4cv748wpd3\.m2\repository\junit\junit\4.11\junit-4.11.jar;C:\Users\4cv748wpd3\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar com.dxz.statement.LightExceptionTest Exception in thread "main" com.dxz.statement.LightException: 测试异常信息 Process finished with exit code 1
2.3、重载fillInStackTrace方法的改进方案--自定义异常时增加writableStackTrace参数,动态取舍是否要stackTrace
先看看Throwable的主要的一些方法:
Throwable有五种构造方法:
Throwable():创建一个无详细信息的Throwable
Throwable(String message):创建一个有详细信息的Throwable
Throwable(String message, Throwable cause):创建一个有详细信息和发生原因的Throwable
protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace):创建一个有详细信息和发生原因的Throwable,并确定是否可以suppression,是否可以writable stack trace。jdk7开始才有。
Throwable(Throwable cause):创建一个有发生原因的Throwable
备注:
suppression:被压抑的异常。想了解更多信息,请参看我的译文“try-with-resources语句”。
strack trace:堆栈跟踪。是一个方法调用过程列表,它包含了程序执行过程中方法调用的具体位置。
Throwable的所有成员方法:
public final void addSuppressed(Throwable exception):把指定的异常加入当前异常的suppressed异常列表,这样就可以把这个异常传递下去。这个方法是线程安全的,通常被try-with-resources语句调用(可以说是专为这种新语句设计的)。jdk7开始才有。如果enableSuppression为false,这个方法无效
public Throwable fillInStackTracze():填充这个执行堆栈跟踪,这个方法把当前线程的堆栈帧的当前状态记录到了这个Throwable对象信息里面。并返回当前Throwable实例。构造器方法都会首先调用这个方法。如果writableStackTrace为false,这个方法无效。
public Throwable getCause():获取cause Throwable信息。其实就是获取底层的异常信息。对应于initCause方法。
public String getLocalizedMessage():对于当前Throwable,创建一个本地化描述。供子类重写。如果子类没有重写这个方法,这个方法返回和getMessage()一样。
public String getMessage():返回当前Throwable的详细信息。
public StackTraceElement[] getStackTrace():获取堆栈跟踪信息,可以通过程序遍历StackTraceElement对象,获取个性化的信息。StackTraceElement的toString方法可以返回标准的堆栈跟踪信息。
public final Throwable[] getSuppressed() :对应于addSuppressed方法,获取suppressed异常列表。这个方法是线程安全的,通常被try-with-resources语句调用(可以说是专为这种新语句设计的)。jdk7开始才有。如果没有,就会返回一个空数组。
public Throwable initCause(Throwable cause):设置引起当前Throwable被抛出的Throwable。只能设置一次cause Throwable,通常在构造方法就设置好了,或者在创建Throwable实例以后马上调用本方法。
public void printStackTrace():把这个Throwable和它的堆栈跟踪信息打印到标准的错误字节流里面
public void printStackTrace(PrintStream s):把这个Throwable和它的堆栈跟踪信息打印到指定的打印字节流里面
public void printStackTrace(PrintWriter s):把这个Throwable和它的堆栈跟踪信息打印到指定的打印字符流里面
public void setStackTrace(StackTraceElement[] stackTrace):手动设置堆栈跟踪信息。这个方法是给RPC框架或其他先进系统设计的,允许客户端覆盖默认的由fillInStackTrace()生成的默认堆栈跟踪信息,如果这个Throwable是从一个序列化字节流读取而来的话。如果writableStackTrace为false,这个方法无效。
public String toString():返回当前Throwable的简短描述。
备注:
所有派生于Throwable类的异常类,基本都没有这些成员方法,也就是说所有的异常类都只是一个标记,记录发生了什么类型的异常(通过标记,编译期和JVM做不同的处理),所有实质性的行为Throwable都具备了。
综上,在一个Throwable里面可以获取什么信息?
- 获取堆栈跟踪信息(源代码中哪个类,哪个方法,第几行出现了问题……从当前代码到最底层的代码调用链都可以查出来)
- 获取引发当前Throwable的Throwable。追踪获取底层的异常信息。
- 获取被压抑了,没抛出来的其他Throwable。一次只能抛出一个异常,如果发生了多个异常,其他异常就不会被抛出,这时可以通过加入suppressed异常列表来解决(JDK7以后才有)。
- 获取基本的详细描述信息
上面的有个参数writableStackTrace可以控制stackTrace是否记录被记录操作等。
重载fillInStackTrace后,如果想要stack的时候反而没有办法了,屏蔽异常栈主要是为了不执行private native Throwable fillInStackTrace(int dummy);这个方法而提高效率,出于这个目的考虑的话有更好的方案,动态决定需不需要异常栈——新增业务异常增加构造函数,用参数决定是否需要异常栈。调用Throwable的构造函数:
protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
package com.dxz.statement; public class LightException2 extends RuntimeException { public LightException2(String msg, boolean writableStackTrace) { super(msg, null, false, writableStackTrace); } }
测试类:
package com.dxz.statement; public class LightExceptionTest2 { public void business() { if(1 == 1) { throw new LightException2("测试异常信息", true/false); } } public static void main(String[] args) { LightExceptionTest2 let = new LightExceptionTest2(); let.business(); } }
Exception in thread "main" com.dxz.statement.LightException2: 测试异常信息 at com.dxz.statement.LightExceptionTest2.business(LightExceptionTest2.java:6) at com.dxz.statement.LightExceptionTest2.main(LightExceptionTest2.java:12)
writableStackTrace=false时
Exception in thread "main" com.dxz.statement.LightException2: 测试异常信息
三、虚拟机为异常做Fast Throw优化
- NullPointerException
- ArithmeticException
- ArrayIndexOutOfBoundsException
- ArraySotreException
- ClassCastException
参考:https://www.zhihu.com/question/21405047/answer/118977314
: