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这样带.的用双引号包裹)。

  1. mvn archetype:generate
  2. -DinteractiveMode=false
  3. -DarchetypeGroupId=org.openjdk.jmh
  4. -DarchetypeArtifactId=jmh-java-benchmark-archetype
  5. -DarchetypeVersion=1.21
  6. -DgroupId=com.jenkov
  7. -DartifactId=first-benchmark
  8. -Dversion=1.0
可以看到生成的项目pom文件中主要是添加了两个jmh

的依赖和设置了maven-shade-plugin的编译方式(负责把项目的所有依赖jar包打入到目标jar包中,与springboot的实现方式类似)。
  1. <dependencies>
  2. <dependency>
  3. <groupId>org.openjdk.jmh</groupId>
  4. <artifactId>jmh-core</artifactId>
  5. <version>${jmh.version}</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.openjdk.jmh</groupId>
  9. <artifactId>jmh-generator-annprocess</artifactId>
  10. <version>${jmh.version}</version>
  11. <scope>provided</scope>
  12. </dependency>
  13. </dependencies>
  14. ...
  15. <plugin>
  16. <groupId>org.apache.maven.plugins</groupId>
  17. <artifactId>maven-shade-plugin</artifactId>
  18. <version>2.2</version>
  19. <executions>
  20. <execution>
  21. <phase>package</phase>
  22. <goals>
  23. <goal>shade</goal>
  24. </goals>
  25. <configuration>
  26. <finalName>${uberjar.name}</finalName>
  27. <transformers>
  28. <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
  29. <mainClass>org.openjdk.jmh.Main</mainClass>
  30. </transformer>
  31. </transformers>
  32. <filters>
  33. <filter>
  34. <!--
  35. Shading signed JARs will fail without this.
  36. http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
  37. -->
  38. <artifact>*:*</artifact>
  39. <excludes>
  40. <exclude>META-INF/*.SF</exclude>
  41. <exclude>META-INF/*.DSA</exclude>
  42. <exclude>META-INF/*.RSA</exclude>
  43. </excludes>
  44. </filter>
  45. </filters>
  46. </configuration>
  47. </execution>
  48. </executions>
  49. </plugin>

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

  1. public class MyBenchmark {
  2.  
  3. @Benchmark
  4. public void testMethod() {
  5. // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
  6. // Put your benchmark code here.
  7. }
  8.  
  9. }

编写基准测试代码

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

  1. public class MyBenchmark {
  2. static AtomicInteger integer = new AtomicInteger();
  3.  
  4. @Benchmark
  5. public void testMethod() {
  6. // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
  7. // Put your benchmark code here.
  8. integer.incrementAndGet();
  9. }
  10. }

JMH打包、运行

项目打包

  1. mvn clean install

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

  1. java -jar benchmark.jar
  2.  
  3. # JMH version: 1.21
  4. # VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
  5. # VM invoker: C:\Java\jdk1.8.0_181\jre\bin\java.exe
  6. # VM options: <none>
  7. # Warmup: 5 iterations, 10 s each
  8. # Measurement: 5 iterations, 10 s each
  9. # Timeout: 10 min per iteration
  10. # Threads: 1 thread, will synchronize iterations
  11. # Benchmark mode: Throughput, ops/time
  12. # Benchmark: org.sample.MyBenchmark.testMethod
  13.  
  14. # Run progress: 0.00% complete, ETA 00:01:40
  15. # Fork: 1 of 1
  16. # Warmup Iteration 1: 81052462.185 ops/s
  17. # Warmup Iteration 2: 80152956.333 ops/s
  18. # Warmup Iteration 3: 81305026.522 ops/s
  19. # Warmup Iteration 4: 81740215.227 ops/s
  20. # Warmup Iteration 5: 82398485.097 ops/s
  21. Iteration 1: 82176523.804 ops/s
  22. Iteration 2: 81818881.730 ops/s
  23. Iteration 3: 82812749.807 ops/s
  24. Iteration 4: 82406672.531 ops/s
  25. Iteration 5: 74270344.512 ops/s
  26.  
  27. Result "org.sample.MyBenchmark.testMethod":
  28. 80697034.477 ±(99.9%) 13903555.960 ops/s [Average]
  29. (min, avg, max) = (74270344.512, 80697034.477, 82812749.807), stdev = 3610709.330
  30. CI (99.9%): [66793478.517, 94600590.437] (assumes normal distribution)
  31.  
  32. # Run complete. Total time: 00:01:41
  33.  
  34. REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
  35. why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
  36. experiments, perform baseline and negative tests that provide experimental control, make sure
  37. the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
  38. Do not assume the numbers tell you what you want them to tell.
  39.  
  40. Benchmark Mode Cnt Score Error Units
  41. 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可能如下:

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3. <modelVersion>4.0.0</modelVersion>
  4.  
  5. <parent>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-parent</artifactId>
  8. <version>2.0.7.RELEASE</version>
  9. <relativePath/>
  10. </parent>
  11. ...
  12. <plugin>
  13. <groupId>org.apache.maven.plugins</groupId>
  14. <artifactId>maven-shade-plugin</artifactId>
  15. <version>2.2</version>
  16. <executions>
  17. <execution>
  18. <!-- 需要在此处添加一个id标签,否则mvn package时会报错 -->
  19. <id>shade-all-dependency-jar</id>
  20. <phase>package</phase>
  21. <goals>
  22. <goal>shade</goal>
  23. </goals>
  24. <configuration>
  25. ...
  26. </configuration>
  27. </execution>
  28. </executions>
  29. </plugin>
  30. ...
  31. </project>

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

  1. public class StringRedisTemplateBenchmark {
  2. StringRedisTemplate redisTemplate;
  3.  
  4. @Setup(Level.Trial)
  5. public void setUp() {
  6. redisTemplate = SpringApplication.run(SpringBootApplicationClass.class).getBean(StringRedisTemplate.class);
  7. }
  8.  
  9. @Benchmark
  10. public void testGet() {
  11. redisTemplate.opsForValue().get("testkey");
  12. }
  13. }
  14.  
  15. @SpringBootApplication
  16. public class SpringBootApplicationClass {
  17.  
  18. }

application.properties

  1. lettuce.pool.maxTotal=50
  2. lettuce.pool.maxIdle=10
  3. lettuce.pool.minIdle=0
  4.  
  5. lettuce.sentinel.master=mymaster
  6. lettuce.sentinel.nodes=10.xx.xx.xx:26379,10.xx.xx.xx:26379
  7. 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: 只计算一次的耗时,一般用来测试冷启动的性能(不设置JVM预热)
  • All: 测试上面的所有指标

默认的benchmark mode是Throughput,可以通过注解的方式设置BenchmarkMode,注解支持放在类或方法上。如下所示设置了Throughput和SampleTime两个Benchmark mode。

  1. @BenchmarkMode({Mode.Throughput, Mode.SampleTime})
  2. public class MyBenchmark {
  3. static AtomicInteger integer = new AtomicInteger();
  4.  
  5. @Benchmark
  6. public void testMethod() {
  7. // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
  8. // Put your benchmark code here.
  9. integer.incrementAndGet();
  10. }
  11. }

Benchmark Time Units

JMH支持设置打印基准测试结果的时间单位,通过@OutputTimeUnit注解的方式设置。

  1. @OutputTimeUnit(TimeUnit.SECONDS)
  2. public class MyBenchmark {
  3. static AtomicInteger integer = new AtomicInteger();
  4.  
  5. @Benchmark
  6. public void testMethod() {
  7. integer.incrementAndGet();
  8. }
  9.  
  10. }

Benchmark State

有时候我们在做基准测试的时候会需要使用一些变量、字段,@State注解是用来配置这些变量的生命周期,@State注解可以放在类上,然后在基准测试方法中可以通过参数的方式把该类对象作为参数使用。@State支持的生命周期类型:

  • Benchmark: 整个基准测试的生命周期,多个线程共用同一份实例对象。该类内部的@Setup @TearDown注解的方法可能会被任一个线程执行,但是只会执行一次。
  • Group: 每一个Group内部共享同一个实例,需要配合@Group @GroupThread使用。该类内部的@Setup @TearDown注解的方法可能会该Group内的任一个线程执行,但是只会执行一次。
  • Thread:每个线程的实例都是不同的、唯一的。该类内部的@Setup @TearDown注解的方法只会被当前线程执行,而且只会执行一次。

被@State标示的类必须满足如下两个要求:

  • 类必须是public的
  • 必须有无参构造函数

State Object @Setup @TearDown

在@Scope注解标示的类的方法上可以添加@Setup和@TearDwon注解。@Setup:用来标示在Benchmark方法使用State对象之前需要执行的操作。@TearDown:用来标示在Benchmark方法之后需要对State对象执行的操作。

如下示例:

  1. @OutputTimeUnit(TimeUnit.SECONDS)
  2. public class MyBenchmark {
  3.  
  4. @Benchmark
  5. public void testMethod(TestAddAndGetState state) {
  6. state.getInteger().incrementAndGet();
  7. }
  8.  
  9. @State(Scope.Benchmark)
  10. public static class TestAddAndGetState {
  11. private AtomicInteger integer;
  12.  
  13. @Setup(Level.Iteration)
  14. public void setup() {
  15. integer = new AtomicInteger();
  16. }
  17.  
  18. public AtomicInteger getInteger() {
  19. return integer;
  20. }
  21. }
  22. }

@Setup、@TearDown支持设置Level级别,Level有三个值:

  • Trial: 每次benchmark前/后执行一次,每次benchmark会包含多轮(Iteration)
  • Iteration: 每轮执行前/后执行一次
  • Invocation: 每次调用测试的方法前/后都执行一次,这个执行频率会很高,一般用不上。

Fork

@Fork注解用来设置启动的JVM进程数量,多个进程是串行的方式启动的,多个进程可以减少偶发因素对测试结果的影响。

Thread

@Thread用来配置执行测试启动的线程数量

Warmup

@Warmup 用来配置预热的时间,如下所示配置预热五轮,每轮1second,也就是说总共会预热5s左右,在这5s内会不停的循环调用测试方法,但是预热时的数据不作为测试结果参考。

  1. @Warmup(iterations = 5, time = 1)

Measurement

@Measurement用来配置基准测试的时间,如下所示配置预热10轮,每轮1second,也就是说总共会测试10s左右,在这10s内会不停的循环调用测试方法,同事测试数据会被基准测试结果参考。

  1. @Measurement(iterations = 5, time = 1)

输出测试结果

jmh支持多种格式的结果输出text, csv, scsv, json, latex
如下打印出json格式的:

  1. java -jar benchmark.jar -rf json

具体实践可参考 HashMap 中7种遍历方式的性能分析

参考

http://openjdk.java.net/projects/code-tools/jmh/

https://www.jianshu.com/p/2a83cc26d0e9

Java基准性能测试--JMH使用介绍的更多相关文章

  1. JMH-大厂是如何使用JMH进行Java代码性能测试的?必须掌握!

    Java 性能测试难题 现在的 JVM 已经越来越为智能,它可以在编译阶段.加载阶段.运行阶段对代码进行优化.比如你写了一段不怎么聪明的代码,到了 JVM 这里,它发现几处可以优化的地方,就顺手帮你优 ...

  2. 在java中使用JMH(Java Microbenchmark Harness)做性能测试

    文章目录 使用JMH做性能测试 BenchmarkMode Fork和Warmup State和Scope 在java中使用JMH(Java Microbenchmark Harness)做性能测试 ...

  3. Java XML解析工具 dom4j介绍及使用实例

    Java XML解析工具 dom4j介绍及使用实例 dom4j介绍 dom4j的项目地址:http://sourceforge.net/projects/dom4j/?source=directory ...

  4. Java 并发和多线程(一) Java并发性和多线程介绍[转]

    作者:Jakob Jenkov 译者:Simon-SZ  校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时 ...

  5. [原创]Java静态代码检查工具介绍

    [原创]Java静态代码检查工具介绍 一  什么是静态代码检查? 静态代码分析是指无需运行被测代码,仅通过分析或检查源程序的语法.结构.过程.接口等来检查程序的正确性,找出代码隐藏的错误和缺陷,如参数 ...

  6. LoadRunner调用Java程序—性能测试-转载

    LoadRunner调用Java程序—性能测试   为了充分利用LoadRunner的场景控制和分析器,帮助我们更好地控制脚本加载过程,从而展现更直观有效的场景分析图表.本次将重点讨论LoadRunn ...

  7. Java并发性和多线程介绍

    java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题,多开线程就好: 快速响应,异步式 ...

  8. java基础-Eclipse开发工具介绍

    java基础-Eclipse开发工具介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 所谓工欲善其事必先利其器,即将身为一名Java开发工程师怎么能没有一款好使的IDE呢?今天就 ...

  9. java基础-Idea开发工具介绍

    java基础-Idea开发工具介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 之前给大家介绍过一款Java的IDE叫eclipse,有些功能用起来不是很得心应手,尤其是在导报的 ...

随机推荐

  1. CRM系统自动化业务流程的好处

    CRM系统的自动化可以简单化企业大部分工作,而且覆盖销售,市场,服务和财务部门,使团队有大量时间花在业务流程上. CRM系统自动化的建立是为了更好地使工作更为简单且便于管理,自动化释放出来员工的时间, ...

  2. SQLFlow数据流分析工具的job功能介绍

    SQLFlow是一款专业的数据血缘关系分析工具,在大型数据仓库中,完整的数据血缘关系可以用来进行数据溯源.表和字段变更的影响分析.数据合规性的证明.数据质量的检查等. 一.SQLFlow 是怎样工作的 ...

  3. Handle详解

    首先通过一个函数启动一个服务器,只提供一个方法并返回Hello World!,当你在浏览器输入http://127.0.0.1:8080,就会看到Hello World. 对于http.ListenA ...

  4. Spring的Xml和JavaConfig 扩展你选哪一个?

    引言 上一篇文章我们有怎么介绍到如何通过XML的形式来定义Spring的扩展<Spring面试高频题如何:自定义XML schema 扩展>,好多人都在吐槽现在都什么年代了,xml还有人再 ...

  5. HTML的表格元素

    一.HTML的表格元素 1.table元素 <table> 标签定义 HTML 表格.简单的 HTML 表格由 table 元素以及一个或多个 tr.th 或 td 元素组成.tr 元素定 ...

  6. 笔记本用HDMI转VGA 使用双屏办公 听语音

    笔记本用HDMI转VGA 使用双屏办公 听语音 原创 | 浏览:1212 | 更新:2019-11-12 12:16 1 2 3 4 5 6 7 分步阅读 笔记本使用 转接头扩展出一块屏幕.使用多屏办 ...

  7. Docker创建镜像以及私有仓库

    Docker的安装及镜像.容器的基本操作详见博客https://blog.51cto.com/11134648/2160257下面介绍Docker创建镜像和创建私有仓库的方法,详细如下: 创建镜像 创 ...

  8. 037.Python的UDP语法

    UDP语法 1 创建一个socket的UDP对象 import socket #创建对象 socket.SOCK_DGRAM 代表UDP协议 sk = socket.socket(type=socke ...

  9. Docker-Compose入门-(转载)

    Compose 是一个用户定义和运行多个容器的 Docker 应用程序.在 Compose 中你可以使用 YAML 文件来配置你的应用服务.然后,只需要一个简单的命令,就可以创建并启动你配置的所有服务 ...

  10. ubuntu中软件的升级管理-(转自Josh_)

    给Ubuntu软件升级命令 sudo apt-get update  --更新软件源 sudo apt-get upgrade -更新已经安装的软件 以非root用户更新系统 sudo: sudo是l ...