谨慎使用Exception

通常在编写业务代码时,会通过下面2种方式来编写各种业务场景。
  1. "返回异常码”:在业务代码中return错误码
  2. 抛出异常+捕获转为返回异常码”:有种观点认为,业务失败异常流程应该基于Exception控制,在这样的项目里就会看到大量的基于业务定义的Exception类,比如UserNotFoundException,LoginFailException什么的。或者把Service层所有的异常分支都包装成一个ServiceException什么的。
这两种方式的性能差异有多大,我们看看下面的示例对比。
示例1:返回异常码和抛出异常+捕获异常转为返回异常码 性能对比
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操作

所有的异常分支都包装成一个ServiceException什么的。这种情况下,throw Exception 就成为一个很常见的事件,这时重载fillInStackTrace 是可以有效益的。重载的目的是屏蔽异常栈主要是为了不执行private native Throwable fillInStackTrace(int dummy);这个方法而提高效率。
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
抛出这样的异常,性能仍然不理想,因为虚拟机对异常的捕获和处理也是非常耗时的操作
 
2.2、重载fillInStackTrace方法缺点:重载fillInStackTrace方法后,异常发生后,没有stack trace信息。
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) {
参数writableStackTrace直接可以决定需不需要执行fillInStackTrace来提高性能。
例如上面LightException的可以修改为如下:
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();
    }
}
结果:
 writableStackTrace=true时
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优化

  默认情况下,虚拟机会对某个方法频繁抛出某些异常做Fast Throw优化。如果检测到在代码中的某个位置连续多次抛出同一类型异常,则决定用Fast Throw方式来抛异常,异常栈信息不会被填写。这种异常抛出的速度非常快,因为不需要在堆里分配内存,也不需要构造完整的异常栈信息。以下异常会使用Fast Throw进行优化:
  • NullPointerException
  • ArithmeticException
  • ArrayIndexOutOfBoundsException
  • ArraySotreException
  • ClassCastException
这种优化方式虽然提高了系统性能,但会导致异常栈消失,从而无法快速定位到错误代码,我们不得不找到更早的日志文件(也许已经被压缩处理了),查看是否包含最初的异常栈。
为了避免这种异常栈优化,可以通过虚拟机参数-XX:-OmitStackTraceInFastThrow来忽略异常优化。

参考:https://www.zhihu.com/question/21405047/answer/118977314

posted on 2021-03-15 10:19  duanxz  阅读(669)  评论(0编辑  收藏  举报