当前位置 博文首页 > Kevin.ZhangCG:Java基准性能测试--JMH使用介绍

    Kevin.ZhangCG:Java基准性能测试--JMH使用介绍

    作者:Kevin.ZhangCG 时间:2021-06-29 19:02

    JMH是什么

    JMH是Java Microbenchmark Harness的简称,一个针对Java做基准测试的工具,是由开发JVM的那群人开发的。想准确的对一段代码做基准性能测试并不容易,因为JVM层面在编译期、运行时对代码做很多优化,但是当代码块处于整个系统中运行时这些优化并不一定会生效,从而产生错误的基准测试结果,而这个问题就是JMH要解决的。

    JMH vs JMeter

    JMeter可能是最常用的性能测试工具。它既支持图形界面,也支持命令行,属于黑盒测试的范畴,对非开发人员比较友好,上手也非常容易。图形界面一般用于编写、调试测试用例,而实际的性能测试建议还是在命令行下运行。

    很多场景下JMeter和JMH都可以做性能测试,但是对于严格意义上的基准测试来说,只有JMH才适合。JMeter的测试结果精度相对JVM较低、所以JMeter不适合于类级别的基准测试,更适合于对精度要求不高、耗时相对较长的操作。

    • JMeter测试精度差: JMeter自身框架比较重,举个例子:使用JMH测试一个方法,平均耗时0.01ms,而使用JMeter测试的结果平均耗时20ms,相差200倍。
    • JMeter内置很多采样器:JMeter内置了支持多种网络协议的采样器,可以在不写Java代码的情况下实现很多复杂的测试。JMeter支持集群的方式运行,方便模拟多用户、高并发压力测试。

    总结: JMeter适合一些相对耗时的集成功能测试,如API接口的测试。JMH适合于类或者方法的单元测试。

    JMH基本用法

    创建JMH项目

    官方推荐为JMH基准测试创建单独的项目,最简单的创建JMH项目的方法就是基于maven项目原型的方式创建(如果是在windows环境下,需要对org.open.jdk.jmh这样带.的用双引号包裹)。

     mvn archetype:generate
              -DinteractiveMode=false
              -DarchetypeGroupId=org.openjdk.jmh
              -DarchetypeArtifactId=jmh-java-benchmark-archetype
              -DarchetypeVersion=1.21
              -DgroupId=com.jenkov
              -DartifactId=first-benchmark
              -Dversion=1.0
    可以看到生成的项目pom文件中主要是添加了两个jmh
    的依赖和设置了maven-shade-plugin的编译方式(负责把项目的所有依赖jar包打入到目标jar包中,与springboot的实现方式类似)。
    <dependencies>
            <dependency>
                <groupId>org.openjdk.jmh</groupId>
                <artifactId>jmh-core</artifactId>
                <version>${jmh.version}</version>
            </dependency>
            <dependency>
                <groupId>org.openjdk.jmh</groupId>
                <artifactId>jmh-generator-annprocess</artifactId>
                <version>${jmh.version}</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    ...
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.2</version>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
                <configuration>
                    <finalName>${uberjar.name}</finalName>
                    <transformers>
                        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <mainClass>org.openjdk.jmh.Main</mainClass>
                        </transformer>
                    </transformers>
                    <filters>
                        <filter>
                            <!--
                                Shading signed JARs will fail without this.
                                http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
                            -->
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
            </execution>
        </executions>
    </plugin>

    生成的项目中已经包含了一个class文件MyBenchmark.java,如下:

    public class MyBenchmark {
    
        @Benchmark
        public void testMethod() {
            // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
            // Put your benchmark code here.
        }
    
    }

    编写基准测试代码

    在上面生成的MyBenchmark类的testMethod中就可以添加基准测试的java代码,举例如下:测试AtomicInteger的incrementAndGet的基准性能。

    public class MyBenchmark {
        static AtomicInteger integer = new AtomicInteger();
    
        @Benchmark
        public void testMethod() {
            // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
            // Put your benchmark code here.
            integer.incrementAndGet();
        }
    }

    JMH打包、运行

    项目打包

    mvn clean install

    运行生成的目标jar包benchmark.jar:

    java -jar benchmark.jar
    
    # JMH version: 1.21
    # VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
    # VM invoker: C:\Java\jdk1.8.0_181\jre\bin\java.exe
    # VM options: <none>
    # 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: org.sample.MyBenchmark.testMethod
    
    # Run progress: 0.00% complete, ETA 00:01:40
    # Fork: 1 of 1
    # Warmup Iteration   1: 81052462.185 ops/s
    # Warmup Iteration   2: 80152956.333 ops/s
    # Warmup Iteration   3: 81305026.522 ops/s
    # Warmup Iteration   4: 81740215.227 ops/s
    # Warmup Iteration   5: 82398485.097 ops/s
    Iteration   1: 82176523.804 ops/s
    Iteration   2: 81818881.730 ops/s
    Iteration   3: 82812749.807 ops/s
    Iteration   4: 82406672.531 ops/s
    Iteration   5: 74270344.512 ops/s
    
    
    Result "org.sample.MyBenchmark.testMethod":
      80697034.477 ±(99.9%) 13903555.960 ops/s [Average]
      (min, avg, max) = (74270344.512, 80697034.477, 82812749.807), stdev = 3610709.330
      CI (99.9%): [66793478.517, 94600590.437] (assumes normal distribution)
    
    
    # Run complete. Total time: 00:01:41
    
    REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
    why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
    experiments, perform baseline and negative tests that provide experimental control, make sure
    the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
    Do not assume the numbers tell you what you want them to tell.
    
    Benchmark                Mode  Cnt         Score          Error  Units
    MyBenchmark.testMethod  thrpt    5  80697034.477 ± 13903555.960  ops/s

    从上面的日志我们大致可以了解到 JMH的基准测试主要经历了下面几个过程:

    1. 打印本次测试的配置,warmup:5轮;measurement:5轮;每轮:10s;启动1个线程做测试;基准测试指标:吞吐量(throughput,单位是s);测试方法MyBenchmark.testMethod
    2. 启动一个JVM进程做基准测试(也可以设置启动多个进程,减少随机因素的误差影响)
    3. 在JVM进程中先执行了5轮的预热(warmup),每轮10s,总共50s的预热时间。预热的数据不作为基准测试的参考。
    4. 测试了5轮,每轮10s,总共50s的测试时间
    5. 汇总测试数据、生成结果报表。最终结论是吞吐量(80697034.477 ±13903555.960 ops/s),其中80697034.477 是结果,13903555.960是误差范围。

    JMH与Springboot

    在对Springboot项目做JMH基准测试时可能会因为maven-shade-plugin插件的问题打包报错,需要在JMH的maven-shade-plugin的插件配置中添加id即可。项目的pom可能如下:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.0.7.RELEASE</version>
            <relativePath/>
        </parent>
    ...
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.2</version>
        <executions>
            <execution>
                <!-- 需要在此处添加一个id标签,否则mvn package时会报错 -->
                <id>shade-all-dependency-jar</id>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
                <configuration>
                    ...
                </configuration>
            </execution>
        </executions>
    </plugin>
    ...
    </project>

    在测试代码中正常基于SpringBootApplication构建ConfigurableApplicationContext从而获取bean的方式获取对象测试即可。

    public class StringRedisTemplateBenchmark  {
        StringRedisTemplate redisTemplate;
                
        @Setup(Level.Trial)
        public void setUp() {
            redisTemplate = SpringApplication.run(SpringBootApplicationClass.class).getBean(StringRedisTemplate.class);
        }
        
        @Benchmark
        public void testGet() {
            redisTemplate.opsForValue().get("testkey");
        }
    }
    
    @SpringBootApplication
    public class SpringBootApplicationClass {
    
    }

    application.properties

    lettuce.pool.maxTotal=50
    lettuce.pool.maxIdle=10
    lettuce.pool.minIdle=0
    
    lettuce.sentinel.master=mymaster
    lettuce.sentinel.nodes=10.xx.xx.xx:26379,10.xx.xx.xx:26379
    lettuce.password=xxxxxx

    JMH注解

    JMH测试的相关配置大多是通过注解的方式体现的。具体每个注解的使用实例也可以参考官网http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

    JMH Benchmark Modes

    JMH benchmark支持如下几种测试模式:

    • Throughput: 吞吐量,测试每秒可以执行操作的次数
    • Average Time: 平均耗时,测试单次操作的平均耗时
    • Sample Time:采样耗时,测试单次操作的耗时,包括最大、最小耗时,已经百分位耗时等
    • Single Shot Time: