Java基准性能测试--JMH使用介绍
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
的依赖和设置了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的基准测试主要经历了下面几个过程:
- 打印本次测试的配置,warmup:5轮;measurement:5轮;每轮:10s;启动1个线程做测试;基准测试指标:吞吐量(throughput,单位是s);测试方法MyBenchmark.testMethod
- 启动一个JVM进程做基准测试(也可以设置启动多个进程,减少随机因素的误差影响)
- 在JVM进程中先执行了5轮的预热(warmup),每轮10s,总共50s的预热时间。预热的数据不作为基准测试的参考。
- 测试了5轮,每轮10s,总共50s的测试时间
- 汇总测试数据、生成结果报表。最终结论是吞吐量(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: 只计算一次的耗时,一般用来测试冷启动的性能(不设置JVM预热)
- All: 测试上面的所有指标
默认的benchmark mode是Throughput,可以通过注解的方式设置BenchmarkMode,注解支持放在类或方法上。如下所示设置了Throughput和SampleTime两个Benchmark mode。
@BenchmarkMode({Mode.Throughput, Mode.SampleTime})
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();
}
}
Benchmark Time Units
JMH支持设置打印基准测试结果的时间单位,通过@OutputTimeUnit注解的方式设置。
@OutputTimeUnit(TimeUnit.SECONDS)
public class MyBenchmark {
static AtomicInteger integer = new AtomicInteger(); @Benchmark
public void testMethod() {
integer.incrementAndGet();
} }
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对象执行的操作。
如下示例:
@OutputTimeUnit(TimeUnit.SECONDS)
public class MyBenchmark { @Benchmark
public void testMethod(TestAddAndGetState state) {
state.getInteger().incrementAndGet();
} @State(Scope.Benchmark)
public static class TestAddAndGetState {
private AtomicInteger integer; @Setup(Level.Iteration)
public void setup() {
integer = new AtomicInteger();
} public AtomicInteger getInteger() {
return integer;
}
}
}
@Setup、@TearDown支持设置Level级别,Level有三个值:
- Trial: 每次benchmark前/后执行一次,每次benchmark会包含多轮(Iteration)
- Iteration: 每轮执行前/后执行一次
- Invocation: 每次调用测试的方法前/后都执行一次,这个执行频率会很高,一般用不上。
Fork
@Fork注解用来设置启动的JVM进程数量,多个进程是串行的方式启动的,多个进程可以减少偶发因素对测试结果的影响。
Thread
@Thread用来配置执行测试启动的线程数量
Warmup
@Warmup 用来配置预热的时间,如下所示配置预热五轮,每轮1second,也就是说总共会预热5s左右,在这5s内会不停的循环调用测试方法,但是预热时的数据不作为测试结果参考。
@Warmup(iterations = 5, time = 1)
Measurement
@Measurement用来配置基准测试的时间,如下所示配置预热10轮,每轮1second,也就是说总共会测试10s左右,在这10s内会不停的循环调用测试方法,同事测试数据会被基准测试结果参考。
@Measurement(iterations = 5, time = 1)
输出测试结果
jmh支持多种格式的结果输出text, csv, scsv, json, latex
如下打印出json格式的:
java -jar benchmark.jar -rf json
具体实践可参考 HashMap 中7种遍历方式的性能分析
参考
Java基准性能测试--JMH使用介绍的更多相关文章
- JMH-大厂是如何使用JMH进行Java代码性能测试的?必须掌握!
Java 性能测试难题 现在的 JVM 已经越来越为智能,它可以在编译阶段.加载阶段.运行阶段对代码进行优化.比如你写了一段不怎么聪明的代码,到了 JVM 这里,它发现几处可以优化的地方,就顺手帮你优 ...
- 在java中使用JMH(Java Microbenchmark Harness)做性能测试
文章目录 使用JMH做性能测试 BenchmarkMode Fork和Warmup State和Scope 在java中使用JMH(Java Microbenchmark Harness)做性能测试 ...
- Java XML解析工具 dom4j介绍及使用实例
Java XML解析工具 dom4j介绍及使用实例 dom4j介绍 dom4j的项目地址:http://sourceforge.net/projects/dom4j/?source=directory ...
- Java 并发和多线程(一) Java并发性和多线程介绍[转]
作者:Jakob Jenkov 译者:Simon-SZ 校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时 ...
- [原创]Java静态代码检查工具介绍
[原创]Java静态代码检查工具介绍 一 什么是静态代码检查? 静态代码分析是指无需运行被测代码,仅通过分析或检查源程序的语法.结构.过程.接口等来检查程序的正确性,找出代码隐藏的错误和缺陷,如参数 ...
- LoadRunner调用Java程序—性能测试-转载
LoadRunner调用Java程序—性能测试 为了充分利用LoadRunner的场景控制和分析器,帮助我们更好地控制脚本加载过程,从而展现更直观有效的场景分析图表.本次将重点讨论LoadRunn ...
- Java并发性和多线程介绍
java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题,多开线程就好: 快速响应,异步式 ...
- java基础-Eclipse开发工具介绍
java基础-Eclipse开发工具介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 所谓工欲善其事必先利其器,即将身为一名Java开发工程师怎么能没有一款好使的IDE呢?今天就 ...
- java基础-Idea开发工具介绍
java基础-Idea开发工具介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 之前给大家介绍过一款Java的IDE叫eclipse,有些功能用起来不是很得心应手,尤其是在导报的 ...
随机推荐
- JAVA基础——运算符号
运算符(java) 算数运算符:+,-,*,/,%(取余),++,-- 赋值运算符:= 关系运算符:<, >, >= ,<= ,== , != 逻辑运算符:&& ...
- python上下文管理协议,即with的详细使用
一.with obj as f: #代码块... 二.执行流程: 1.with obj --->触发obj.__enter__(),需要在obj里写__enter__(self),在它里边写返回 ...
- IT菜鸟之路由器基础配置(静态、动态、默认路由)
路由器:连接不同网段的设备 企业级路由和家用级路由的区别: 待机数量不同(待机量) 待机量:同时接通的终端设备的数量 待机量的值越高,路由的性能越好 别墅级路由,表示信号好,和性能无关 交换机:背板带 ...
- Prometheus存储原理及数据备份还原
prometheus将采集到的样本以时间序列的方式保存在内存(TSDB 时序数据库)中,并定时保存到硬盘中.与zabbix不同,zabbix会保存所有的数据,而prometheus本地存储会保存15天 ...
- Linux中级之负载均衡(lvs,nginx,haproxy)、中间件
一.负载均衡的概念 1.系统的扩展方式: scale up:向上扩展 scale out:向外扩展 2.集群类型: LB(Load Balancing).HA(high availability) ...
- 三大主流开源硬件对比:Arduino vs Raspberry Pi vs BeagleBone
http://www.elecfans.com/emb/361236_3.html 下文摘自上面的链接 软硬件整合是今年一再被提及的话题,如今我们也可以看到不少硬件创业的成功案例,比如Jawbone ...
- 《Java架构师的最佳实践》生产环境JVM调优之空间担保失败引起的FullGC
1 问题现象 应用prod-xxx-k8s,在内存足够的情况下,仍然会产生偶发FullGC的问题. JVM配置如下: -Xmx8192m -Dhsf.server.max.poolsize=2500 ...
- 国内镜像源 sources
Ubuntu18.04源 cat > /etc/apt/sources.list <<eof # 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释 deb ...
- GO语言复合类型04---映射
package main import "fmt" /* 映射(map)是键值对(key-value)数据的集合 根据键key可以快速检索值value 键值的类型可以是任意的,ke ...
- QT绘制简易表盘
1.简介 最近学习了一下QT,熟悉了一段时间后发现它的功能还是挺强大的,同时也比较方便用户上手去使用.现在就基于最近学习的内容,实现一个简易的带指针旋转功能的表盘.文中表盘的实现是基于QT的QPain ...