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,如下:
package org.sample;
import org.openjdk.jmh.annotations.Benchmark;
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的基準性能。
package org.sample;
import org.openjdk.jmh.annotations.Benchmark;
import java.util.concurrent.atomic.AtomicInteger;
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 {
}
resources/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
JMH Visual
jmh可以基于jmh生成的json格式測試結果展示出圖形化的界面,參考網站:https://jmh.morethan.io/#details
參考
http://tutorials.jenkov.com/java-performance/jmh.html
http://openjdk.java.net/projects/code-tools/jmh/
https://yangbingdong.com/2018/spring-boot-learning-testing/