zoukankan      html  css  js  c++  java
  • 谨慎使用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:javajdk1.8.0_111injava.exe "-javaagent:C:Program FilesJetBrainsIntelliJ IDEA 2019.3.2libidea_rt.jar=51627:C:Program FilesJetBrainsIntelliJ IDEA 2019.3.2in" -Dfile.encoding=UTF-8 -classpath C:javajdk1.8.0_111jrelibcharsets.jar;C:javajdk1.8.0_111jrelibdeploy.jar;C:javajdk1.8.0_111jrelibextaccess-bridge-64.jar;C:javajdk1.8.0_111jrelibextcldrdata.jar;C:javajdk1.8.0_111jrelibextdnsns.jar;C:javajdk1.8.0_111jrelibextjaccess.jar;C:javajdk1.8.0_111jrelibextjfxrt.jar;C:javajdk1.8.0_111jrelibextlocaledata.jar;C:javajdk1.8.0_111jrelibext
    ashorn.jar;C:javajdk1.8.0_111jrelibextsunec.jar;C:javajdk1.8.0_111jrelibextsunjce_provider.jar;C:javajdk1.8.0_111jrelibextsunmscapi.jar;C:javajdk1.8.0_111jrelibextsunpkcs11.jar;C:javajdk1.8.0_111jrelibextzipfs.jar;C:javajdk1.8.0_111jrelibjavaws.jar;C:javajdk1.8.0_111jrelibjce.jar;C:javajdk1.8.0_111jrelibjfr.jar;C:javajdk1.8.0_111jrelibjfxswt.jar;C:javajdk1.8.0_111jrelibjsse.jar;C:javajdk1.8.0_111jrelibmanagement-agent.jar;C:javajdk1.8.0_111jrelibplugin.jar;C:javajdk1.8.0_111jrelib
    esources.jar;C:javajdk1.8.0_111jrelib
    t.jar;D:studyjmhenchmark-demo	argetclasses;C:Users4cv748wpd3.m2
    epositoryorgopenjdkjmhjmh-core1.25jmh-core-1.25.jar;C:Users4cv748wpd3.m2
    epository
    etsfjopt-simplejopt-simple4.6jopt-simple-4.6.jar;C:Users4cv748wpd3.m2
    epositoryorgapachecommonscommons-math33.2commons-math3-3.2.jar;C:Users4cv748wpd3.m2
    epositoryjunitjunit4.11junit-4.11.jar;C:Users4cv748wpd3.m2
    epositoryorghamcresthamcrest-core1.3hamcrest-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

  • 相关阅读:
    Java 源码刨析
    qemu-guest-agent详解
    Java 源码刨析
    NTP服务解析
    virsh常见命令笔记
    Ansible之playbook
    ansible模块详解
    HashMap详解
    Mysql-Incorrect string value
    web开发中前后端传值
  • 原文地址:https://www.cnblogs.com/duanxz/p/14536046.html
Copyright © 2011-2022 走看看