• 【性能测试JMH】SpirngBoot结合 JMH进行性能测试 调优


    高性能问题


    代码单元的性能测试、代码优化


    按照软件工程的思想,要想完成一个项目,只要做好准备工作,编码的工作量是很小的,比如搭建一个复杂的web项目,直接分模块,比如Cfeng的预上线网站cfeng.net目前提供的几个服务就是不同的几个模块,借助JPA、Spring Boot就可以迅速完成项目的构建,个人项目麻烦的反而是前端的页面

    在初期阶段,we可能为了完成任务,比如完成一个身份鉴定,可以使用多层if, 同时也可以选择使用switch,但是二者的性能和速度you 真的考虑过吗?

    这里提几个ms的问题: 简单的代码的性能问题

    if 和 switch 谁的性能更好?

    FastJSON和 GSON谁性能更好?

    程序使用Spring 框架 和不使用Spring 框架 哪个性能更好? 【虽然现在几乎不可能绕开Spring了,servlet,手动new】

    HashMap 初始化的时候需要指定 初始的空间大小吗? 【安全不是这里讨论的】

    HashMap 获取容器大小 需要更多的延时吗?

    JDK的 炫技的Lambda表达式 是否需要消耗更多的性能?

    MyBatis-plus 和 Spring-Data-JPA 哪个框架的性能更好?

    Spring-Data-Redis和分布式的Rdission哪个效率高?

    … 就各种比较性能【MinIO和FastDFS】… 怎么回答? 回答的思路?

    性能测试代码全部放在GITEE的cfeng-test-demo

    JMH java microbenchmark harness java单元性能测试

    在编写项目的时候,作为决策者选择框架的时候就难免需要考虑性能问题,多个框架都可以解决问题,这里就面临选择,而编代码过程更会遇到更细节的问题,也会涉及到性能的比较 : 比如 基于STOMP的聊天服务,存储当前用户endpoint,是使用Set还是Map? 如何Choose? Set中使用泛型是Set《JSONObject》和Set《JavaBean》数据大小同时谁更快?

    分别对两个单元使用JMH进行性能测试之后,得到的结果包含每s或者每ns的最大执行数, 得出谁的性能更好

    JMH是OpenJDK开发的基准测试工具,【之前的Junit是测试结果的】,一般用于代码的性能调优,精度达到ns,适用于Java和其余基于JVM的language,是JDK9之后自带的(貌似还是需要),和web性能测试JMeter不同,JMH测试对象可以为任何方法,不仅限于REST API

    <dependency>
    	<groupId>org.openjdk.jmhgroupId>
        <artifactId>jmh-coreartifactId>
        <version>1.23version>
    dependency>
    
    <dependency>
    	<groupId>org.openjdk.jmhgroupId>
        <artifactId>jmh-generator-annprocessartifactId>
        <version>1.23version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    JMH只要基于方法层面进行性能测试,类似与Junit单元测试,测试的目的不同,为代码优化过程;

    JMH的使用场景:

    • 准确知道某方法执行的时间,执行时间和输入之间的相关性
    • 对比接口的不同实现在给定条件下的吞吐量【性能】
    • 查看多少百分比的请求在多少时间内完成

    JMH使用

    首先贴一段实践的代码

    import org.openjdk.jmh.annotations.Benchmark;
    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;
    
    /**
     * @author Cfeng
     * @date 2022/9/1
     */
    
    public class JMHExampleTest {
    
        /**
         * 这里基准函数
         */
        @Benchmark
        public void jmhMethod() {
    
        }
    
        public static void main(String[] args) throws RunnerException {
            Options opt = new OptionsBuilder()
                            .include(JMHExampleTest.class.getSimpleName())  //基准类
                            .forks(1)
                            .build();
            new Runner(opt).run();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    JMH编译期间生成基准代码,在基准列表中将待测性能的方法注册为基准数值,该待测方法应该是公开的且可以抛出异常; 抛出异常之后JMH的Runner就会执行错误,直接结束,进入下一个基准测试; 衡量的基准,将空函数作为基准进行参考【空函数没有内容,执行速度最快】

    JMH 的运行依赖于org.openjdk.jmh.runner.options.Options的配置, 配置Options之后,就可以通过org.openjdk.jmh.runner.Runner进行运行

    • include: 运行时包含的基准类
    • forks: 指定方法重复执行的次数

    执行后可以看到大量的迭代,大量的吞吐量,每隔函数的开销

    # Warmup: 5 iterations, 10 s each
    # Measurement: 5 iterations, 10 s each
    # Timeout: 10 min per iteration
    # Threads: 1 thread, will synchronize iterations
    # Benchmark mode: Throughput, ops/time
    # Benchmark: com.Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod
    
    # Run progress: 0.00% complete, ETA 00:01:40
    # Fork: 1 of 1
    # Warmup Iteration   1: 2432867074.037 ops/s
    # Warmup Iteration   2: 2560473580.920 ops/s
    # Warmup Iteration   3: 2602717289.592 ops/s
    # Warmup Iteration   4: 2585859810.812 ops/s
    # Warmup Iteration   5: 2588578460.594 ops/s
    Iteration   1: 2606056999.216 ops/s
    Iteration   2: 2469027629.800 ops/s
    Iteration   3: 2595788786.098 ops/s
    Iteration   4: 2591829783.488 ops/s
    Iteration   5: 2600675478.647 ops/s
    
    Result "com.Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod":
      2572675735.450 ±(99.9%) 224052512.720 ops/s [Average]
      (min, avg, max) = (2469027629.800, 2572675735.450, 2606056999.216), stdev = 58185726.044
      CI (99.9%): [2348623222.730, 2796728248.170] (assumes normal distribution)
    
    Benchmark                                              Mode  Cnt           Score           Error  Units
    Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod  thrpt    5  2572675735.450 ± 224052512.720  ops/s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    ops 为Operations Per Second 每秒的操作数; 可以看到空函数的速度非常快, 可以看到空函数的执行为每s 执行25亿次

    @BenchMarkMode 设置基准测试的模式 【方法或者类】

    设置运行基准测试的模式,可以选择放在方法上面,只对该方法生效,在BenchMarkMode内部设置Mode的模式

    • Mode.Throughput : 吞吐量模式,获得单位时间的操作数量,连续运行@BenchMark的方法,计算所有的工作线程的总吞吐量
    • Mode.AverageTime: 平均时间模式, 获得每次操作的平均时间,计算所有工作线程的平均时间
    • Mode.SimpleTime: 时间采样模式, 对每一个操作函数的时间进行采样,连续运行@BenchMark的函数,随机抽取运行所需要的时间
    • Mode.SingleShotTime: 单次触发模式, 测试单次操作的时间,连续运行@BenchMark函数,只运行一次并计算时间: 该模式只是运行一次@BenchMark函数,所以需要预热, 如果基准数值小,使用SimpleTime模式采样
    • Mode.All : 无模式,采用所有的基准模式,效果最好

    @OutPutTimeUnit 报告结果的默认时间单位【类、方法】

    可以放在类或者方法上面,设置测试的结果的显示的时间的单位,可以是哦那个java.util.current.TimeUnit进行设置

    @OutPutTimeUnit(TimeUnit.MILLSECOUNDS)

    @Warmup 预热,设置具体的配置参数如次数,时间等

    JVM进程启动时,类加载器将所需要的所有类加载入内存,Bootstrap Class 核心类库,比如JRE、lib等; Extension Class 由相关的ExtClassLoader加载, Application Class 由AppClassLoader负责加载

    类加载过程完毕后,所有类会进入JVM cache中,但是其他与JVM启动无关类没有加载、懒加载,当应用的第一个请求到来(比如controller的一个处理器),会触发相关类第一次加载,这个过程比较耗时, 对于低延迟应用必须要避免

    采用特定的策略处理加载逻辑,保证第一次请求的快速响应,称为JVM预热

    设置具体的预热的参数

    • iterations: 预热的迭代次数
    • Time: 预热的时间
    • timeUnit: 预热的时间单位
    • batchSize: 每个操作的基准方法的调用次数 (batch 一批)

    @Measurement 类似预热,但是设置的是测量时的

    测量的参数和上面的预热的参数相同

    @Fork 整体测试几次

    就像之前的在main函数中设置Options中设置fork的参数,之前通过new Runner 配置参数进行run

    @State 设置配置对象的作用域,定义线程之间的共享程度

    可以设置测试状态对象的多线程的共享程度

    • Scope.Benchmark: 基准状态范围, 基准作用域: 相同类型的所有实例在所有工作线程之间共享; ---- Spring多为无状态单例Bean,可以直接所有的线程共享 此状态上面的对象的SetUp方法和TearDown方法都是一个工作线程执行,每个级别一次,没有其他线程可以操作状态对象
    • Scope.Group: 组状态范围、组作用域: 相同类型的所有实例在同一组中的所有的线程之间共享,每一个线程组都将提供自己的状态对象 【组内共享,组间隔离】 该状态对象上面的SetUp方法和TearDown由一个组线程执行
    • Scope.Thread: 线程状态范围,线程作用域: 相同类型的所有实例都不同(不是单例的),在同一个基准中注入了多个状态对象,此状态的SetUp和TearDown方法由单个工作线程独占执行

    @Setup 线程执行前的配置函数、初始化

    该注解只能在配置函数上面,Setup方法只由一个可以访问State对象的线程执行,一般就是一个特定的工作线程执行,如果状态共享,那么就可能由不同的线程执行(Thread 作用域)

    @TearDown 测试后处理操作 【方法】

    放置在方法上面进行测试后处理操作,一般测试后清理资源

    @BenchMark 标记测试基准 【方法】

    放在方法上面表明测试的基准

    @OperationsPerInvocation 与基准进行多操作通信,运行JMH调整

    这里就可以测试循环内部的代码

    @BenchMark
    @OperationsPerInvocation(10)
    public void test() {
        for(int i = 0; i < 10; i ++) {
            //xxx
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    SpringBoot中使用JMH

    SpringBoot中使用的区别就是需要获取容器中的Bean,进行测试, 在测试中,使用@Setup进行初始化, 使用SpringApplication.run(XXXX) 获取容器,获取测试所需要的Bean

    package com.Jning.cfengtestdemo.jmhTest;
    
    import com.Jning.cfengtestdemo.DemoApplication;
    import com.Jning.cfengtestdemo.controller.TestUserController;
    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 org.springframework.boot.SpringApplication;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ConfigurableApplicationContext;
    
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author Cfeng
     * @date 2022/9/1
     */
    
    @BenchmarkMode(Mode.AverageTime) //平均时间模式
    @State(Scope.Benchmark) //使用的SpringBoot容器,都是无状态单例Bean,无安全问题,可以直接使用基准作用域BenchMark
    @OutputTimeUnit(TimeUnit.NANOSECONDS) //这里是ns为单位
    @Fork(1)  //整体平均执行1次
    @Warmup(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS) //预热1s
    @Measurement(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS) //测试也是1s、五遍
    public class JMHExampleTest {
    
        //springBoot容器
        private ApplicationContext context;
    
        //待测试的TestUserController
        private TestUserController userController;
    
        /**
         * 初始化,获取springBoot容器,run即可,同时得到相关的测试对象
         */
        @Setup
        public void init() {
            //容器获取
            context = SpringApplication.run(DemoApplication.class);
            //获取对象
            userController = context.getBean(TestUserController.class);
        }
    
        @Benchmark
        public void getUserList() {
            List userList =  userController.queryAll();
        }
    
    
        @Benchmark
        public void testIncrement() {
            context.getBeanDefinitionCount();
        }
    
        /**
         * 测试的后处理操作,关闭容器,资源清理
         */
        @TearDown
        public void down() {
    //        System.out.println("结束测试,后处理操作");
            //使用子类ConfigurableApplicationContext关闭
            ((ConfigurableApplicationContext)context).close();
        }
    
    
        public static void main(String[] args) throws RunnerException {
    //        Options opt = new OptionsBuilder()
    //                        .include(JMHExampleTest.class.getSimpleName())  //基准类
    //                        .forks(1) //重复执行的次数
    //                        .build();
    
            //使用注解之后只需要配置一下include即可,fork和warmup、measurement都是注解
            Options opt = new OptionsBuilder()
                            .include(".*" + JMHExampleTest.class.getSimpleName() + ".*")
                            .build();
            new Runner(opt).run();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    多使用注解代替在Options中进行配置

    Benchmark                                                 Mode  Cnt       Score        Error  Units
    Jning.cfengtestdemo.jmhTest.JMHExampleTest.getUserList    avgt    5  621247.176 ± 109697.833  ns/op
    Jning.cfengtestdemo.jmhTest.JMHExampleTest.testIncrement  avgt    5       1.860 ±      0.237  ns/op
    
    • 1
    • 2
    • 3

    这里可以看到测试两个方法,第一个是controller接口的获取所有的用户, 第二个为获取容器bean的数量,第一个每次操作需要621247ns 相比getCount来说消耗庞大, 这是因为方法1需要进行数据库查询,需要进行数据库连接,2不需要

    同理可以使用StringBuilder和StringBuffer,StringBuilder运行的速度更快【Buffer更安全】

    其他的性能问题也可以进行比较得出,比如

    Lambda表达式,一般情况下增强for循环比Stream更快, 但是预热后Lambda、stream可能更快

    JMH建议在打包之后再使用,利用jar进行命令行方式的启动测试,因为IDEA本身也会消耗资源

    界面化和日志输出都很消耗性能, 比如空函数和System.out.print(XXX)就差距巨大,在高并发的操作时就避免输出日志

  • 相关阅读:
    Exception_json反序列化失败_JSONException
    Windows环境下Redis安装与配置的两种方式
    SpringCloud原理-OpenFeign篇(三、FeignClient的动态代理原理)
    Python文件操作:操作文件的1个函数3个方法使用、readline按行读取文件、文件指针(详细图文)
    【JAVASE】JDK8新特性
    用DIV+CSS技术设计我的家乡网站(web前端网页制作课作业)南宁绿城之都
    2000万的行数在2023年仍然是 MySQL 表的有效软限制吗?
    【深入Java原子类:高性能并发编程的技巧与实践】
    高可用集群HA、LVS+Keepalived、健康检测
    ABB MPRC086444-005数字输入模块
  • 原文地址:https://blog.csdn.net/a23452/article/details/126680840