Java Throwable异常堆栈收集的性能问题

在项目中,大家通常都喜欢使用 throw new BusinessException() 来阻断错误的参数或者业务逻辑异常等已知异常,反馈给用户。比如用户未绑定手机号、用户的参数异常等情况。在大部分情况下,使用异常终止流程都不是很好的方式,但是奈何在SpringBoot 环境下,@RestControllerAdvice 实在是香,不需要定义很多返回状态码。所以被大多数人使用在项目中。

但是使用此方式是具有一定的性能损耗的,因为在基类Throwable 的fillInStackTrace()方法会不断收集各层的调用链路,而SpringMVC又是在外层的Tomcat到DispatcherServlet到应用自定义的各种intercepter层层调用,调用堆栈通常较长。所以多多少少具有一定的性能消耗,下面进行了一个基本测试。

第一个for循环是为了给JVM预热。
第二个模拟普通的异常。
第三个模拟重写了fillInStackTrace()方法的异常。

/**
 * @author imyzt
 * @date 2021/01/02
 * @description 异常收集测试
 */
public class TestMain {


    public static void main(String[] args) {


        int idx = 1000022;


        long t1Start = System.currentTimeMillis();
        for (int i = 0; i < idx; i++) {
            Test test = new Test("aa" + i);
        }
        long t1end = System.currentTimeMillis();
        System.out.println(t1end - t1Start);




        long t2Start = System.currentTimeMillis();
        for (int i = 0; i < idx; i++) {
            try {
                throw new ThrowableTest("aa" + i);
            } catch (ThrowableTest throwableTest) {
            }
        }
        long t2end = System.currentTimeMillis();
        System.out.println(t2end - t2Start);






        long t3Start = System.currentTimeMillis();
        for (int i = 0; i < idx; i++) {
            try {
                throw new ThrowableTest2("aa" + i);
            } catch (ThrowableTest2 throwableTest) {


            }
        }
        long t3end = System.currentTimeMillis();
        System.out.println(t3end - t3Start);
    }




    public static class Test {


        private String name;


        public Test(String name) {
            this.name = name;
        }
    }


    public static class ThrowableTest extends RuntimeException {


        private String msg;


        public ThrowableTest(String msg) {
            super(msg);
            this.msg = msg;
        }
    }


    public static class ThrowableTest2 extends RuntimeException {


        private String msg;


        public ThrowableTest2(String msg) {
            super(msg);
            this.msg = msg;
        }


        @Override
        public synchronized Throwable fillInStackTrace() {
            return this;
        }
    }
}

耗时对比:

88
1631
74

通过结果可以看出,即使在非Web环境下都有如此巨大的性能差距,web环境下冗长的调用链路差距只会更大,大家可使用web环境下进行测试。

但是不是说有性能损耗就不用, 也需要结合业务进行权衡,比如可以将类似于参数错误等无需通过调用堆栈判断的、但是出现频率有很高的的异常进行重写基类的fillInStackTrace()方法,使其不收集堆栈,但是此举会使出现异常后无法排查,所以只能使用在无需通过堆栈判断问题的场景下用于提升性能。