Java异常及相关调用性能测试

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
性能测试 PTS,5000VUM额度
简介: # 问题 ## 参数校验问题 见到有人讨论java里的异常性能是好是坏,在业务代码里要不要用异常处理. 例如一些请求参数,到底是应该人工用if/else判断,还是通过异常统一处理,还是通过注解校验,抑或是其他方式呢? 这些方式对系统的性能会有什么实际影响呢? ## 日志运行信息问题 一般认为,java异常之所以慢,是因为需要获取当前的运行栈信息,而异常机制本身是

问题

参数校验问题

见到有人讨论java里的异常性能是好是坏,在业务代码里要不要用异常处理.

例如一些请求参数,到底是应该人工用if/else判断,还是通过异常统一处理,还是通过注解校验,抑或是其他方式呢?

这些方式对系统的性能会有什么实际影响呢?

日志运行信息问题

一般认为,java异常之所以慢,是因为需要获取当前的运行栈信息,而异常机制本身是常规性能消耗.

进而想到,一些通用的日志框架,比如log4j,logback,都是通过运行栈获取抛出异常的代码方法和运行行数的,官方文档也标注此类信息的打印会有较大的性能消耗.

而且我们也是通过类似的方式对jws的日志进行了增强.

那么性能到底会有有多差?

测试场景

运行环境

  • jdk8 + idea 2016
  • mac mini 2014 中配(硬盘非常慢,如有其它运行结果请联系)
  • 非空机运行,所以结果不是非常准确
  • jvm配置-client -Xcomp -Xmx1024m -Xms1024m,每次调用完后主动通知gc一次

    • 编译模式跟解析模式的结果相差非常大

异常创建

一般都说,使用异常之所以慢,是因为获取运行栈慢,那到底有多慢,慢在哪里,如果不获取异常栈的话速度又如何?

通过Throwable的源码可以得知,大部分的够着函数里都会调用java.lang.Throwable#fillInStackTrace()方法设置当前运行的异常栈,而这个方法慢的原因有:

  • 方法本身是synchronized,也就是对象级的同步
  • 这个方法需要有一个native调用
  • 需要获取当前运行栈的所有信息

另外,Throwable的很多方法也是synchronized的.

(不过线程栈信息本身有缓存,理论上第二次调用会快一些,而且异常一般不会在多个线程中处理)

所以理论上来说,只要把异常栈填充这一步去掉,异常对象应该是跟普通对象是差不多的.

刚好Throwalbe及各种Exception都有一个特殊的构造函数,可以跳过异常栈的获取.

protected Throwable(String message, Throwable cause,
                        boolean enableSuppression,
                        boolean writableStackTrace) {
        ...
    }

串行创建

普通对象 HashMap 省略异常栈 普通异常
1000000个对象 35 38 48 1444
10000000个对象 30 62 99 10525
100000000个对象 269 593 936 112262

可以看到,关闭异常栈获取后,异常对象的创建跟普通对象基本一致,而获取异常栈则多了不止一个数量级的耗时.

并行创建

普通对象 HashMap 省略异常栈 普通异常
10个线程,每个线程10000000个对象 223 728 534 73946
100个线程,每个线程1000000个对象 251 605 533 85106
500个线程,每个线程200000个对象 264 780 793 68589

在并行模式下的结果跟串行模式几乎一致.

异常处理

如果异常对象的创建的性能没有问题,那么try/catch块呢?

循环内外catch性能对比

根据java的try/catch运行原理,理论上来说,在不抛出异常的情况下,try/catch没有性能的消耗

没有try/catch 循环外try/catch 循环内try/catch 循环内3次try/catch
运行10000000次 10 12 9 13
运行100000000次 43 49 53 45
运行500000000次 221 223 222 229

可以看到,当没有发生异常时,try/catch并不消耗性能.

throw/catch处理耗时

那么,对异常的throw/catch需要消耗多少性能呢?

没有throw/catch throw/catch
运行10000000次 11 36
运行100000000次 51 113
运行500000000次 252 519

而当抛出异常时,try/catch本身的性能消耗也只是普通的代码性能消耗.

运行栈获取

通过运行栈获取当前执行的方法名和行数

通用的日志框架里都是通过运行栈去获取日志调用代码的方法名/行数等信息的,而且通过上述测试可以知道,这种方式会对性能代码比较大的影响.

通过往某个文件打印日志对运行信息获取的性能做测试.(使用非缓存式打印,一般日志框架默认都没有使用缓存打印)

串行

普通日志打印 运行信息日志打印
打印10000行 297 533
打印100000行 535 2132
打印500000行 2576 8598

在串行模式下,通过运行栈获取调用信息确实会比普通的日志打印耗时多,可是可能因为磁盘io关系,性能差异没有异常对象与普通对象的差异大.

并行

普通日志打印 运行信息日志打印
10个线程,每个线程打印100000行 7199 19511
20个线程,每个线程打印50000行 7600 20053
50个线程,每个线程打印20000行 7832 19031

在并发环境下,由于性能进一步被文件io限制,性能差异进一步缩小.

对系统QPS的影响

以上所有的测试都只是验证在一个非常纯粹的环境下的性能表现,其中甚至会有jvm的一些优化措施.

而实际业务处理中,会有框架,网络,业务处理等多种因素会影响系统的性能.

所以通过在本机搭建的一个基于jws的web工程来模拟实际的业务服务,通过ab测试相关的场景性能.

请求参数校验测试

建立一个有3个参数的请求,通过ab工具测试if/else和try/catch两种参数校验方式对性能的影响.

(数据单位:qps)

if/else参数校验 try/catch优化异常参数校验 try/catch普通异常参数校验
串行,5000请求 166.140 163.773 164.636
10并发,5000请求 206.59 206.22 203.06
50并发,5000请求 205.74 205.64 202.80

请求日志打印测试

根据统计平台,生产环境80%请求的耗时基本都在10ms附近,测试用例通过sleep的方式模拟每次请求有10ms的业务处理.

(考虑到机器问题,在本机器上10ms的业务处理应该算是一个很小的值了)

(数据单位:qps)

每次请求打印10行日志 每次请求打印20行日志 每次请求打印50行日志
串行,5000请求 普通日志 56.67 54.85 49.76
运行上线文日志 51.39 47.44 37.10
10并发,5000请求 普通日志 59.54 57.27 52.78
运行上线文日志 55.81 48.96 38.57

结论

  1. 虽然即使经过优化后的异常性能也有一定的消耗,可是在异常校验这种场景下,一来因为发生频率小,二来因为执行次数少(一个请求只会出现一次try/catch),对业务请求的性能几乎没有影响.
  2. 而通过运行栈实现的日志增强,因为日志打印本身的高频率,所以对业务系统性能有一定的影响.

测试代码

异常测试用例

public class ExceptionCreate {
    
    private static final String MSG = "test";
    
    public static void run() throws Throwable {
        serial(1000000);
        serial(10000000);
        serial(100000000);
        
        parallel(10000000, 10);
        parallel(5000000, 20);
        parallel(2000000, 50);
    }
    
    private static void serial(int count) throws Throwable {
        Util.print("---------- 串行创建[%s]个%s对象 ----------", count, "普通");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < count; i++) {
                obj = new Util.NormalObject(MSG);
            }
            Util.print(obj);
        });
        Util.print("---------- 串行创建[%s]个%s对象 ----------", count, "HashMap");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < count; i++) {
                obj = new HashMap<String, String>();
            }
            Util.print(obj);
        });
        Util.print("---------- 串行创建[%s]个%s对象 ----------", count, "省略异常栈Throwable");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < count; i++) {
                obj = new Util.WithoutStackThrowalbe(MSG);
            }
            Util.print(obj);
        });
        Util.print("---------- 串行创建[%s]个%s对象 ----------", count, "省略异常栈异常");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < count; i++) {
                obj = new Util.WithoutStackException(MSG);
            }
            Util.print(obj);
        });
        Util.print("---------- 串行创建[%s]个%s对象 ----------", count, "普通异常");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < count; i++) {
                obj = new RuntimeException(MSG);
            }
            Util.print(obj);
        });
    }
    
    private static void parallel(int countEachThread, int concurrentLevel) throws Throwable {
        Util.print("---------- [%s]并行创建[%s]个%s对象 ----------", concurrentLevel, countEachThread, "普通");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < countEachThread; i++) {
                obj = new Util.NormalObject(MSG);
            }
            Util.print(obj);
        }, concurrentLevel);
        Util.print("---------- [%s]并行创建[%s]个%s对象 ----------", concurrentLevel, countEachThread, "HashMap");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < countEachThread; i++) {
                obj = new HashMap<String, String>();
            }
            Util.print(obj);
        }, concurrentLevel);
        Util.print("---------- [%s]并行创建[%s]个%s对象 ----------", concurrentLevel, countEachThread, "省略异常栈Throwable");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < countEachThread; i++) {
                obj = new Util.WithoutStackThrowalbe(MSG);
            }
            Util.print(obj);
        }, concurrentLevel);
        Util.print("---------- [%s]并行创建[%s]个%s对象 ----------", concurrentLevel, countEachThread, "省略异常栈异常");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < countEachThread; i++) {
                obj = new Util.WithoutStackException(MSG);
            }
            Util.print(obj);
        }, concurrentLevel);
        Util.print("---------- [%s]并行创建[%s]个%s对象 ----------", concurrentLevel, countEachThread, "普通异常");
        Util.useTime(() -> {
            Object obj = null;
            for (int i = 0; i < countEachThread; i++) {
                obj = new RuntimeException(MSG);
            }
            Util.print(obj);
        }, concurrentLevel);
    }
}

try/catch测试用例

public class TryCatch {
    public static void run() throws Throwable {
        tryCatchPlace(10000000);
        tryCatchPlace(100000000);
        tryCatchPlace(500000000);
        
        throwCatch(10000000);
        throwCatch(100000000);
        throwCatch(500000000);
    }
    
    public static void tryCatchPlace(int runCount) throws Throwable {
        Util.print("---------- 没有try/catch运行[%s]次耗时 ----------", runCount);
        Util.useTime(() -> {
            long total = 0;
            for (int i = 0; i < runCount; i++) {
                total = total + i;
            }
            Util.print(total);
        });
        Util.print("---------- 循环外try/catch运行[%s]次耗时 ----------", runCount);
        Util.useTime(() -> {
            long total = 0;
            try {
                for (int i = 0; i < runCount; i++) {
                    total = total + i;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            Util.print(total);
        });
        Util.print("---------- 循环内1次try/catch运行[%s]次耗时 ----------", runCount);
        Util.useTime(() -> {
            long total = 0;
            for (int i = 0; i < runCount; i++) {
                try {
                    total = total + i;
                } catch (RuntimeException e) {
                    e.printStackTrace();
                }
            }
            Util.print(total);
        });
        Util.print("---------- 循环内3次try/catch运行[%s]次耗时 ----------", runCount);
        Util.useTime(() -> {
            long total = 0;
            for (int i = 0; i < runCount; i++) {
                try {
                    try {
                        try {
                            total = total + i;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } catch (RuntimeException e) {
                    e.printStackTrace();
                }
            }
            Util.print(total);
        });
    }
    
    private static void throwCatch(int runCount) throws Throwable {
        RuntimeException existException = new RuntimeException("test");
        Util.print("---------- 没有发生throw/catch运行[%s]次耗时 ----------", runCount);
        Util.useTime(() -> {
            long total = 0;
            for (int i = 0; i < runCount; i++) {
                try {
                    if (i > -1) {
                        total = total + i;
                    } else {
                        throw existException;
                    }
                } catch (RuntimeException ignore) {
                    total = total + i;
                }
            }
            Util.print(total);
        });
        
        Util.print("---------- throw/catch运行[%s]次耗时 ----------", runCount);
        Util.useTime(() -> {
            long total = 0;
            for (int i = 0; i < runCount; i++) {
                try {
                    if (i < -1) {
                        total = total + i;
                    } else {
                        throw existException;
                    }
                } catch (RuntimeException ignore) {
                    total = total + i;
                }
            }
            Util.print(total);
        });
    }
}

日志测试用例

public class Log {
    
    public static void run() throws Throwable {
        util.Logger.redirectSystemOut(Logger.PRINT_FILE_PATH, () -> {
            serial(10000);
            serial(100000);
            serial(500000);
            
            parallel(100000, 10);
            parallel(50000, 20);
            parallel(20000, 50);
        });
    }
    
    private static void serial(int runCount) throws Throwable {
        Util.print("---------- 普通日志串行打印运行[%s]次耗时 ----------", runCount);
        Util.useTime(() -> {
            for (int i = 0; i < runCount; i++) {
                util.Logger.print("test");
            }
        });
        
        
        Util.print("---------- 调用栈日志串行打印运行[%s]次耗时 ----------", runCount);
        Util.useTime(() -> {
            for (int i = 0; i < runCount; i++) {
                util.Logger.printWithCaller("test");
            }
        });
    }
    
    public static void parallel(int runCountEachThread, int concurrentLevel) throws Throwable {
        Util.print("---------- 普通日志[%s]并行打印运行[%s]次耗时 ----------", concurrentLevel, runCountEachThread);
        Util.useTime(() -> {
            for (int i = 0; i < runCountEachThread; i++) {
                util.Logger.print("test");
            }
        }, concurrentLevel);
        
        Util.print("---------- 调用栈日志[%s]并行打印运行[%s]次耗时 ----------", concurrentLevel, runCountEachThread);
        Util.useTime(() -> {
            for (int i = 0; i < runCountEachThread; i++) {
                util.Logger.printWithCaller("test");
            }
        }, concurrentLevel);
    }
    
}

参数校验服务测试用例

(此用例基于jws,非jws环境无法运行)

public class ExceptionController extends Controller {
    
    public static void noException(String a, Integer b, Boolean c, Long serviceTime) throws Exception {
        if (StringUtils.isEmpty(a)) {
            Util.render(ImmutableMap.of("code", 500, "msg", "a不能为空"));
        }
        if (b == null || b < 1) {
            Util.render(ImmutableMap.of("code", 500, "msg", "b不能为空且要大于0"));
        }
        if (c == null || Boolean.FALSE.equals(c)) {
            Util.render(ImmutableMap.of("code", 500, "msg", "c不能为空且必须为true"));
        }
        if (serviceTime != null && serviceTime > 0) {
            Thread.sleep(serviceTime);
        }
        Util.render(ImmutableMap.of("a", a, "b", b, "c", c));
    }
    
    public static void fastException(String a, Integer b, Boolean c, Long serviceTime) throws Exception {
        try {
            if (StringUtils.isEmpty(a)) {
                throw new FastException(500, "a不能为空");
            }
            if (b == null || b < 1) {
                throw new FastException(500, "b不能为空且要大于0");
            }
            if (c == null || Boolean.FALSE.equals(c)) {
                throw new FastException(500, "c不能为空且必须为true");
            }
            if (serviceTime != null && serviceTime > 0) {
                Thread.sleep(serviceTime);
            }
            Util.render(ImmutableMap.of("a", a, "b", b, "c", c));
        } catch (FastException e) {
            Util.render(ImmutableMap.of("code", e.code, "msg", e.getMessage()));
        }
    }
    
    public static void normalException(String a, Integer b, Boolean c, Long serviceTime) throws Exception {
        try {
            if (StringUtils.isEmpty(a)) {
                throw new NormalException(500, "a不能为空");
            }
            if (b == null || b < 1) {
                throw new NormalException(500, "b不能为空且要大于0");
            }
            if (c == null || Boolean.FALSE.equals(c)) {
                throw new NormalException(500, "c不能为空且必须为true");
            }
            if (serviceTime != null && serviceTime > 0) {
                Thread.sleep(serviceTime);
            }
            Util.render(ImmutableMap.of("a", a, "b", b, "c", c));
        } catch (NormalException e) {
            Util.render(ImmutableMap.of("code", e.code, "msg", e.getMessage()));
        }
    }
    
    private static class FastException extends RuntimeException{
        public final int code;
        
        public FastException(int code, String msg) {
            super(msg, null, false, false);
            this.code = code;
        }
    }
    
    private static class NormalException extends RuntimeException{
        public final int code;
        
        public NormalException(int code, String msg) {
            super(msg);
            this.code = code;
        }
    }
}

日志服务测试用例

(此用例基于jws,非jws环境无法运行)

public class LogController extends Controller {
    static {
        try {
            Logger.redirectSystemOut(Logger.PRINT_FILE_PATH);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
    
    public static void normalLog(int logCount) throws Throwable {
        for (int i = 0; i < logCount; i++) {
            Logger.print("test");
        }
        Thread.sleep(10);
        Util.render("ok");
    }
    
    public static void enhanceLog(int logCount) throws Throwable {
        for (int i = 0; i < logCount; i++) {
            Logger.printWithCaller("test");
        }
        Thread.sleep(10);
        Util.render("ok");
    }
}

其他代码

普通测试入口

public class TestPerformance {
    public static void main(String args[]) throws Throwable {
        ExceptionCreate.run();
        TryCatch.run();
        Log.run();
    }
}

日志组件

public class Logger {
    public static final String PRINT_FILE_PATH = "/Users/jason/Downloads/print.txt";
    private static volatile PrintStream usingOut = System.out;
    
    public static void redirectSystemOut(String filePath, CheckedRunnable runnable) throws Throwable {
        usingOut = new PrintStream(new FileOutputStream(filePath));
        runnable.run();
        usingOut.close();
        usingOut = System.out;
    }
    
    public static void redirectSystemOut(String filePath) throws Throwable {
        usingOut = new PrintStream(new FileOutputStream(filePath));
    }
    
    public static void print(String msg, Object... args) {
        usingOut.println(String.format(msg, args));
    }
    
    public static void printWithCaller(String msg, Object... args) {
        String[] infos = inferCallerInfos();
        print("[%s.%s] %s", infos[0], infos[1], String.format(msg, args));
    }
    
    private static String[] inferCallerInfos() {
        String[] infos = null;
        try {
            StackTraceElement stack[] = (new Throwable()).getStackTrace();
            
            if (null != stack && stack.length >= 4) {
                infos = new String[3];
                StackTraceElement traceElement = stack[2];
                //取具体类名
                String classNames[] = traceElement.getClassName().split("\\.");
                String className = classNames[classNames.length - 1];
                
                String methodName = traceElement.getMethodName();
                int lineNumber = traceElement.getLineNumber();
                
                infos[0] = className;
                infos[1] = methodName;
                infos[2] = String.valueOf(lineNumber);
            }
        } catch (Exception e) {
            jws.Logger.error(e, "[LoggerUtil.inferCallerInfos] happen error");
            infos = null;
        }
        return infos;
    }
}

工具类

public class Util {
    public static final Result EMPTY_RESULT = new Result() {
        @Override
        public void apply(Http.Request request, Http.Response response) {
        }
    };
    
    public static void useTime(CheckedRunnable runnable) throws Throwable {
        long beginTime = System.nanoTime();
        runnable.run();
        System.out.println(String.format("use time:[%s]", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - beginTime)));
        System.gc();
    }
    
    public static void useTime(CheckedRunnable task, int concurrentLevel) throws Throwable {
        CountDownLatch begin = new CountDownLatch(1);
        CountDownLatch finish = new CountDownLatch(concurrentLevel);
        ExecutorService executor = Executors.newFixedThreadPool(concurrentLevel);
        Stream.generate(() -> 1).limit(concurrentLevel).forEach(integer -> {
            executor.submit(Unchecked.runnable(() -> {
                begin.await();
                task.run();
                finish.countDown();
            }));
        });
        useTime(() -> {
            begin.countDown();
            finish.await();
        });
        executor.shutdown();
    }
    
    public static void print(Object msg, Object... params) {
        if (msg instanceof String) {
            System.out.println(String.format((String) msg, params));
        }
    }
    
    public static void render(Map<String, Object> params) throws IOException {
        render(JSON.toJSONString(params));
    }
    
    public static void render(String result) throws IOException {
        Http.Response response = Http.Response.current();
        response.setContentTypeIfNotSet("application/json; charset=utf-8");
        response.out.write(result.getBytes());
        response.out.close();
        throw EMPTY_RESULT;
    }
    
    public static class NormalObject {
        private String msg;
        
        public NormalObject(String msg) {
            this.msg = msg;
        }
    }
    
    public static class WithoutStackThrowalbe extends Throwable {
        public WithoutStackThrowalbe(String message) {
            super(message, null, false, false);
        }
    }
    
    public static class WithoutStackException extends Throwable {
        public WithoutStackException(String message) {
            super(message, null, false, false);
        }
    }
}
相关实践学习
通过性能测试PTS对云服务器ECS进行规格选择与性能压测
本文为您介绍如何利用性能测试PTS对云服务器ECS进行规格选择与性能压测。
相关文章
|
6天前
|
Java 测试技术 API
Java 新手入门:Java单元测试利器,Mock详解
Java 新手入门:Java单元测试利器,Mock详解
28 1
|
11天前
|
网络协议 Java 编译器
Java常见异常及对应解决办法
Java常见异常及对应解决办法
29 10
|
4天前
|
存储 Java 程序员
|
4天前
|
Cloud Native 数据处理
项目环境测试问题之当异步任务在运行过程中抛出非预期的异常会导致后果如何解决
项目环境测试问题之当异步任务在运行过程中抛出非预期的异常会导致后果如何解决
|
11天前
|
Java 编译器 程序员
Java面试题-异常
Java面试题-异常
25 6
|
11天前
|
网络协议 Java 数据库连接
13 Java异常(异常过程解析、throw、throws、try-catch关键字)
13 Java异常(异常过程解析、throw、throws、try-catch关键字)
32 2
|
17天前
|
存储 Java 编译器
Java内存区域与内存溢出异常 - 运行时数据区
【8月更文挑战第2天】Java运行时数据区包括:1) 程序计数器:记录线程执行字节码的行号,线程私有;2) Java虚拟机栈:描述方法执行的内存模型,线程私有,深度过大抛出`StackOverflowError`;3) 本地方法栈:服务于Native方法,线程私有;4) Java堆:所有线程共享,对象实例在此分配内存;5) 方法区:存储类信息、常量等数据;6) 运行时常量池:方法区的一部分,存放字面量和符号引用。不当使用如无限创建对象或过度递归调用会导致各种内存溢出错误。
|
25天前
|
监控 算法 Java
|
4天前
|
测试技术
单元测试问题之在单元测试中,方法的返回值或异常,如何验证
单元测试问题之在单元测试中,方法的返回值或异常,如何验证
|
5天前
|
Java 测试技术
Java SpringBoot Test 单元测试中包括多线程时,没跑完就结束了
Java SpringBoot Test 单元测试中包括多线程时,没跑完就结束了
10 0