JMH--一款由OpenJDK開發的基準測試工具
阿新 • • 發佈:2020-08-29
# 什麼是JMH
JMH 是 OpenJDK 團隊開發的一款基準測試工具,一般用於程式碼的效能調優,精度甚至可以達到納秒級別,適用於 java 以及其他基於 JVM 的語言。和 Apache JMeter 不同,**JMH 測試的物件可以是任一方法,顆粒度更小**,而不僅限於rest api。
使用時,我們只需要通過配置告訴 JMH 測試哪些方法以及如何測試,JMH 就可以為我們**自動生成基準測試的程式碼**。
# JMH生成基準測試程式碼的原理
我們只需要通過配置(主要是註解)告訴 JMH 測試哪些方法以及如何測試,JMH 就可以為我們自動生成基準測試的程式碼。
那麼 JMH 是如何做到的呢?
要使用 JMH,**我們的 JMH 配置專案必須是 maven 專案**。在一個 JMH配置專案中,我們可以在`pom.xml`看到以下配置。JMH 自動生成基準測試程式碼的本質就是**使用 maven 外掛的方式,在 package 階段對配置專案進行解析和包裝**。
```xml
org.apache.maven.plugins
maven-shade-plugin
2.2
package
shade
${uberjar.name}
org.openjdk.jmh.Main
*:*
META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
```
# 從入門例子開始
下面會先介紹整個使用流程,再通過一個入門例子來演示如何使用 JMH。
## 步驟
如果我有一個 A 專案,我希望對這個專案裡的某些方法進行 JMH 測試,可以這麼做:
1. **建立單獨的 JMH 配置專案**B。
新建一個獨立的配置專案 B(**建議使用 archetype 生成,可以確保配置正確**),B 依賴了 A。
當然,我們也可以直接將專案 A 作為 JMH 配置專案,但這樣做會導致 JMH 滲透到 A 專案中,所以,最好不要這麼做。
2. **配置專案B**。
在 B 專案裡面,我們可以使用 JMH 的註解或物件來指定測試哪些方法以及如何測試,等等。
3. **構建和執行**。
在正確配置 pom.xml 的前提下,使用 mvn 命令打包 B 專案,JMH 會為我們自動生成基準測試程式碼,並單獨打包成 benchmarks.jar。執行 benchmarks.jar,基準測試就可以跑起來了。
注意,JMH 也支援使用 Java API 的方式來執行,但官方並不推薦,所以,本文也不會介紹。
下面開始入門例子。
## 專案環境說明
maven:3.6.3
作業系統:win10
JDK:8u231
JMH:1.25
## 建立 JMH 配置專案
為了保證配置的正確性,建議使用 archetype 生成 JMH 配置專案。cmd 執行下面這段程式碼:
```powershell
mvn archetype:generate ^
-DinteractiveMode=false ^
-DarchetypeGroupId=org.openjdk.jmh ^
-DarchetypeArtifactId=jmh-java-benchmark-archetype ^
-DarchetypeVersion=1.25 ^
-DgroupId=cn.zzs.jmh ^
-DartifactId=jmh-test01 ^
-Dversion=1.0.0
```
注:如果使用 linux,請將“^”替代為“\”。
命令執行後,在當前目錄下生成了一個 maven 專案,如下。這個專案就是本文說到的 JMH 配置專案。這裡 archetype 還提供了一個例子`MyBenchmark`。
```powershell
└─jmh-test01
│ pom.xml
│
└─src
└─main
└─java
└─cn
└─zzs
└─jmh
MyBenchmark.java
```
## 配置 JMH 配置專案
### 配置 pom.xml
因為是使用 archetype 生成的專案,所以pom.xml 檔案已經包含了比較完整的 JMH 配置,如下(省略部分)。如果自己手動建立配置專案,則需要拷貝下面這些內容。
```xml
org.openjdk.jmh
jmh-core
${jmh.version}
org.openjdk.jmh
jmh-generator-annprocess
${jmh.version}
provided
UTF-8
1.25
1.8
benchmarks
org.apache.maven.plugins
maven-shade-plugin
3.2.1
package
shade
${uberjar.name}
org.openjdk.jmh.Main
*:*
META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
```
### 配置Benchmark方法
專案裡的 MyBenchmark 類就是一個簡單的示例,testMethod 方法就是一個 Benchmark 方法。我們可以直接在 testMethod 方法中編寫測試程式碼,也可以呼叫父專案的方法。
testMethod 方法上加了`@Benchmark`註解,**`@Benchmark`註解用來告訴 JMH 在 mvn package 時生成這個方法的基準測試程式碼**。
當然,我們還可以增加其他的配置來影響 JMH 如何生成基準測試程式碼,這裡暫時不展開。
```java
package cn.zzs.jmh;
import org.openjdk.jmh.annotations.Benchmark;
public class MyBenchmark {
@Benchmark
public void testMethod() {
// place your benchmarked code here
}
}
```
### 打包和執行
分別執行以下命令,完成對專案的打包:
```powershell
cd jmh-test01
mvn clean package
```
這時,target 目錄下,不僅生成了專案本身的 jar 包,還生成了一個 benchmarks.jar。這個包就是 JMH 為我們生成的基準測試程式碼。
```powershell
└─jmh-test01
│ pom.xml
│
├─src
│ └─main
│ └─java
│ └─cn
│ └─zzs
│ └─jmh
│ MyBenchmark.java
│
└─target
benchmarks.jar
jmh-test01-1.0.0.jar
```
執行以下命令:
```powershell
java -jar target/benchmarks.jar
```
這時,我們的基準測試就開始運行了。
```powershell
D:\growUp\git_repository\java-tools\jmh-demo\jmh-test01>java -jar target/benchmarks.jar
# JMH version: 1.25
# VM version: JDK 1.8.0_231, Java HotSpot(TM) 64-Bit Server VM, 25.231-b11
# VM invoker: D:\growUp\installation\jdk1.8.0_231\jre\bin\java.exe
# VM options:
# 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: cn.zzs.jmh.MyBenchmark.testMethod
# Run progress: 0.00% complete, ETA 00:08:20
# Fork: 1 of 5
# Warmup Iteration 1: 3955731078.669 ops/s
# Warmup Iteration 2: 3910971792.656 ops/s
# Warmup Iteration 3: 3881001464.578 ops/s
# Warmup Iteration 4: 3916172600.571 ops/s
# Warmup Iteration 5: 3956321997.093 ops/s
Iteration 1: 3942596162.384 ops/s
Iteration 2: 3962073081.983 ops/s
Iteration 3: 3956347169.335 ops/s
Iteration 4: 3935835073.222 ops/s
Iteration 5: 3934716909.315 ops/s
# ······
# Run progress: 80.00% complete, ETA 00:01:40
# Fork: 5 of 5
# Warmup Iteration 1: 3398845405.179 ops/s
# Warmup Iteration 2: 3716777120.646 ops/s
# Warmup Iteration 3: 3414803497.798 ops/s
# Warmup Iteration 4: 3621211396.229 ops/s
# Warmup Iteration 5: 3616308570.681 ops/s
Iteration 1: 3898056365.287 ops/s
Iteration 2: 3935143498.460 ops/s
Iteration 3: 3943901632.014 ops/s
Iteration 4: 3906292827.077 ops/s
Iteration 5: 3918607665.065 ops/s
Result "cn.zzs.jmh.MyBenchmark.testMethod":
3949010528.035 ±(99.9%) 16881035.344 ops/s [Average]
(min, avg, max) = (3898056365.287, 3949010528.035, 3975167080.768), stdev = 22535699.213
CI (99.9%): [3932129492.691, 3965891563.378] (assumes normal distribution)
# Run complete. Total time: 00:08:21
Benchmark Mode Cnt Score Error Units
MyBenchmark.testMethod thrpt 25 3949010528.035 ± 16881035.344 ops/s
```
在頭部分列印了`MyBenchmark.testMethod`這個 Benchmark 方法的配置資訊,如下:
```powershell
# JMH version: 1.25
# VM version: JDK 1.8.0_231, Java HotSpot(TM) 64-Bit Server VM, 25.231-b11
# VM invoker: D:\growUp\installation\jdk1.8.0_231\jre\bin\java.exe
# VM options:
# Warmup: 5 iterations, 10 s each ---------------預熱5個迭代,每個迭代10s
# Measurement: 5 iterations, 10 s each------------正式測試5個迭代,每個迭代10s
# Timeout: 10 min per iteration-------------------每個迭代的超時時間10min
# Threads: 1 thread, will synchronize iterations--使用1個執行緒測試
# Benchmark mode: Throughput, ops/time------------使用吞吐量作為測試指標
# Benchmark: cn.zzs.jmh.MyBenchmark.testMethod
```
在最後列印了這個 Benchmark 方法的測試結果,如下。它的吞吐是 3949010528.035 ± 16881035.344 ops/s。注意,**一個 Benchmark 的測試結果是沒有意義的,只有多個 Benchmark 對比才可能得出結論**。
```powershell
Benchmark Mode Cnt Score Error Units
MyBenchmark.testMethod thrpt 25 3949010528.035 ± 16881035.344 ops/s
```
# 詳細配置
通過上面的入門例子簡單介紹瞭如何使用 JMH,接下來將繼續對Benchmark 方法的配置 。針對這一點,官方沒有給出具體的文件,而是提供了 30 多個示例程式碼供我們學習[JMH Samples](http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/) 。
這些示例程式碼並不好讀懂,尤其是涉及到 JVM 的部分。其實,只要我們讀懂 1-11、20 的例子就行,這些例子已經足夠我們日常使用。
至於其他的,大多是介紹 JVM 或者本地機器的某些機制將影響到測試的準確性,以及通過什麼方法減少這些影響,非常難懂。本文不會介紹這部分內容,大部分情況下,JMH 已經儘量為我們遮蔽這些因素帶來的影響,我們只要使用預設配置就可以。
以下只針對 1-11、20 的例子進行總結和補充。有誤的地方,歡迎指正。
在介紹以下內容之前,這裡先介紹下一個 Benchmark 方法的組成部分(只是一個大致結果,並不準確),如下。要很好地理解後面的內容,最後掌握這個結構。
```java
//Benchmark
public void Benchmark01(){
// ······
// 預熱
// 每個迴圈為一個iteration
for(iterations){
// 每個迴圈為一個invocation
while(!timeout){
// 呼叫我們的測試方法
}
}
// ······
// 測試
// 每個迴圈為一個iteration
for(iterations){
// 每個迴圈為一個invocation,這裡會統計每次invocation的開銷
while(!timeout){
// 呼叫我們的測試方法
}
}
// ······
}
```
## @Benchmark
`@Benchmark`用於告訴 JMH 哪些方法需要進行測試,只能註解在方法上,有點類似 junit 的`@Test`。在測試專案進行 package 時,JMH 會針對註解了`@Benchmark`的方法生成 Benchmark 方法程式碼。
```java
@Benchmark
public void wellHelloThere() {
// this method was intentionally left blank.
}
```
通常情況下,每個 Benchmark 方法都執行在獨立的程序中,互不干涉。
## @BenchmarkMode
`@BenchmarkMode`用於指定當前 Benchmark 方法使用哪種模式測試。JMH 提供了4種不同的模式,用於輸出不同的結果指標,如下:
| 模式 | 描述 |
| -------------- | ------------------------------------------------------------ |
| Throughput | 吞吐量,ops/time。單位時間內執行操作的平均次數 |
| AverageTime | 每次操作所需時間,time/op。執行每次操作所需的平均時間 |
| SampleTime | 同 AverageTime。區別在於 SampleTime 會統計取樣 x% 達到了多少 time/op,如下。
| | SingleShotTime | 同 AverageTime。區別在於 SingleShotTime 只執行一次操作。這種模式的結果存在較大隨機性。 | `@BenchmarkMode`支援陣列,也就是說可以為同一個方法同時指定多種模式,生成基準測試程式碼時,JMH 將按照不同模式分別生成多個獨立的 Benchmark 方法。另外,我們可以使用`@OutputTimeUnit`來指定時間單位,可以精確到納秒級別。 ```java /* * 使用一種模式 */ @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public void measureThroughput() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(100); } /* * 使用多種模式 */ @Benchmark @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}) @OutputTimeUnit(TimeUnit.MICROSECONDS) public void measureMultiple() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(100); } /* * 使用所有模式 */ @Benchmark @BenchmarkMode(Mode.All) @OutputTimeUnit(TimeUnit.MICROSECONDS) public void measureAll() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(100); } ``` ## @Warmup和@Measurement `@Warmup` 和`@Measurement`分別用於配置預熱迭代和測試迭代。其中,iterations 用於指定迭代次數,time 和 timeUnit 用於每個迭代的時間,batchSize 表示執行多少次 Benchmark 方法為一個 invocation。 ```java @Benchmark @Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS, batchSize = 10) @Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS, batchSize = 10) public double measure() { //······ } ``` ## @State 個人理解,State 就是被注入到 Benchmark 方法中的物件,它的資料和方法可以被 Benchmark 方法使用。在 JMH 中,註解了`@State`的類在測試專案進行 package 時可以被注入到 Benchmark 方法中。 ### 配置方式 State 的配置方式有兩種。 第一種是 Benchmark 不在 State 的類裡。這時需要在測試方法的入參列表裡顯式注入該 State。 ```java public class JMHSample_03_States { @State(Scope.Benchmark) public static class BenchmarkState { volatile double x = Math.PI; } @State(Scope.Thread) public static class ThreadState { volatile double x = Math.PI; } @Benchmark public void measureUnshared(ThreadState state) { state.x++; } @Benchmark public void measureShared(BenchmarkState state) { state.x++; } } ``` 第二種是 Benchmark 在 State 的類裡。這時不需要在測試方法的入參列表裡顯式注入該 State。 ```java @State(Scope.Thread) public class JMHSample_04_DefaultState { double x = Math.PI; @Benchmark public void measure() { x++; } } ``` ### Scope Scope 是`@State`的屬性,用於描述 State 的作用範圍,主要有三種: | scope | 描述 | | --------- | ------------------------------------------------------------ | | Benchmark | Benchmark 中所有執行緒都使用同一個 State | | Group | Benchmark 中同一 Benchmark 組(使用@Group標識,後面再講)使用一個 State | | Thread | Benchmark 中每個執行緒使用同一個 State | ### @Setup 和 @TearDown 這兩個註解只能定義在註解了 State 裡,其中,`@Setup`類似於 junit 的`@Before`,而`@TearDown`類似於 junit 的`@After`。 ```java @State(Scope.Thread) public class JMHSample_05_StateFixtures { double x; @Setup(Level.Iteration) public void prepare() { System.err.println("init............"); x = Math.PI; } @TearDown(Level.Iteration) public void check() { System.err.println("destroy............"); assert x > Math.PI : "Nothing changed?"; } @Benchmark public void measureRight() { x++; } } ``` 這兩個註解註釋的方法的呼叫時機,主要受 Level 的控制,JMH 提供了三種 Level,如下: 1. Trial Benchmark 開始前或結束後執行,如下。Level 為 Benchmark 的 Setup 和 TearDown 方法的開銷不會計入到最終結果。 ```java //Benchmark public void Benchmark01(){ // call Setup method // 每個迴圈為一個iteration for(iterations){ // 每個迴圈為一個invocation,這裡會統計每次invocation的開銷 while(!timeout){ // 呼叫我們的測試方法 } } // call TearDown method } ``` 2. Iteration Benchmark 裡每個 Iteration 開始前或結束後執行,如下。Level 為 Iteration 的 Setup 和 TearDown 方法的開銷不會計入到最終結果。 ```java //Benchmark public void Benchmark01(){ // 每個迴圈為一個iteration for(iterations){ // call Setup method // 每個迴圈為一個invocation,這裡會統計每次invocation的開銷 while(!timeout){ // 呼叫我們的測試方法 } // call TearDown method } } ``` 3. Invocation Iteration 裡每次方法呼叫開始前或結束後執行,如下。**Level 為 Invocation 的 Setup 和 TearDown 方法的開銷將計入到最終結果**。 ```java //Benchmark public void Benchmark01(){ // 每個迴圈為一個iteration for(iterations){ // 每個迴圈為一個invocation,這裡會統計每次invocation的開銷 while(!timeout){ // call Setup method // 呼叫我們的測試方法 // call TearDown method } } } ``` 以上內容基本可以滿足 JMH 的日常使用需求,至於其他示例的內容,後面有空再做補充。 # 參考資料 [openjdk官網](http://openjdk.java.net/projects/code-tools/jmh/) > 相關原始碼請移步:[https://github.com/ZhangZiSheng001/jmh-demo](https://github.com/ZhangZiSheng001/jmh-demo) >本文為原創文章,轉載請附上原文出處連結:[https://www.cnblogs.com/ZhangZiSheng001/p/13581390.html](https://www.cnblogs.com/ZhangZiSheng001/p/1358139
| | SingleShotTime | 同 AverageTime。區別在於 SingleShotTime 只執行一次操作。這種模式的結果存在較大隨機性。 | `@BenchmarkMode`支援陣列,也就是說可以為同一個方法同時指定多種模式,生成基準測試程式碼時,JMH 將按照不同模式分別生成多個獨立的 Benchmark 方法。另外,我們可以使用`@OutputTimeUnit`來指定時間單位,可以精確到納秒級別。 ```java /* * 使用一種模式 */ @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public void measureThroughput() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(100); } /* * 使用多種模式 */ @Benchmark @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}) @OutputTimeUnit(TimeUnit.MICROSECONDS) public void measureMultiple() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(100); } /* * 使用所有模式 */ @Benchmark @BenchmarkMode(Mode.All) @OutputTimeUnit(TimeUnit.MICROSECONDS) public void measureAll() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(100); } ``` ## @Warmup和@Measurement `@Warmup` 和`@Measurement`分別用於配置預熱迭代和測試迭代。其中,iterations 用於指定迭代次數,time 和 timeUnit 用於每個迭代的時間,batchSize 表示執行多少次 Benchmark 方法為一個 invocation。 ```java @Benchmark @Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS, batchSize = 10) @Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS, batchSize = 10) public double measure() { //······ } ``` ## @State 個人理解,State 就是被注入到 Benchmark 方法中的物件,它的資料和方法可以被 Benchmark 方法使用。在 JMH 中,註解了`@State`的類在測試專案進行 package 時可以被注入到 Benchmark 方法中。 ### 配置方式 State 的配置方式有兩種。 第一種是 Benchmark 不在 State 的類裡。這時需要在測試方法的入參列表裡顯式注入該 State。 ```java public class JMHSample_03_States { @State(Scope.Benchmark) public static class BenchmarkState { volatile double x = Math.PI; } @State(Scope.Thread) public static class ThreadState { volatile double x = Math.PI; } @Benchmark public void measureUnshared(ThreadState state) { state.x++; } @Benchmark public void measureShared(BenchmarkState state) { state.x++; } } ``` 第二種是 Benchmark 在 State 的類裡。這時不需要在測試方法的入參列表裡顯式注入該 State。 ```java @State(Scope.Thread) public class JMHSample_04_DefaultState { double x = Math.PI; @Benchmark public void measure() { x++; } } ``` ### Scope Scope 是`@State`的屬性,用於描述 State 的作用範圍,主要有三種: | scope | 描述 | | --------- | ------------------------------------------------------------ | | Benchmark | Benchmark 中所有執行緒都使用同一個 State | | Group | Benchmark 中同一 Benchmark 組(使用@Group標識,後面再講)使用一個 State | | Thread | Benchmark 中每個執行緒使用同一個 State | ### @Setup 和 @TearDown 這兩個註解只能定義在註解了 State 裡,其中,`@Setup`類似於 junit 的`@Before`,而`@TearDown`類似於 junit 的`@After`。 ```java @State(Scope.Thread) public class JMHSample_05_StateFixtures { double x; @Setup(Level.Iteration) public void prepare() { System.err.println("init............"); x = Math.PI; } @TearDown(Level.Iteration) public void check() { System.err.println("destroy............"); assert x > Math.PI : "Nothing changed?"; } @Benchmark public void measureRight() { x++; } } ``` 這兩個註解註釋的方法的呼叫時機,主要受 Level 的控制,JMH 提供了三種 Level,如下: 1. Trial Benchmark 開始前或結束後執行,如下。Level 為 Benchmark 的 Setup 和 TearDown 方法的開銷不會計入到最終結果。 ```java //Benchmark public void Benchmark01(){ // call Setup method // 每個迴圈為一個iteration for(iterations){ // 每個迴圈為一個invocation,這裡會統計每次invocation的開銷 while(!timeout){ // 呼叫我們的測試方法 } } // call TearDown method } ``` 2. Iteration Benchmark 裡每個 Iteration 開始前或結束後執行,如下。Level 為 Iteration 的 Setup 和 TearDown 方法的開銷不會計入到最終結果。 ```java //Benchmark public void Benchmark01(){ // 每個迴圈為一個iteration for(iterations){ // call Setup method // 每個迴圈為一個invocation,這裡會統計每次invocation的開銷 while(!timeout){ // 呼叫我們的測試方法 } // call TearDown method } } ``` 3. Invocation Iteration 裡每次方法呼叫開始前或結束後執行,如下。**Level 為 Invocation 的 Setup 和 TearDown 方法的開銷將計入到最終結果**。 ```java //Benchmark public void Benchmark01(){ // 每個迴圈為一個iteration for(iterations){ // 每個迴圈為一個invocation,這裡會統計每次invocation的開銷 while(!timeout){ // call Setup method // 呼叫我們的測試方法 // call TearDown method } } } ``` 以上內容基本可以滿足 JMH 的日常使用需求,至於其他示例的內容,後面有空再做補充。 # 參考資料 [openjdk官網](http://openjdk.java.net/projects/code-tools/jmh/) > 相關原始碼請移步:[https://github.com/ZhangZiSheng001/jmh-demo](https://github.com/ZhangZiSheng001/jmh-demo) >本文為原創文章,轉載請附上原文出處連結:[https://www.cnblogs.com/ZhangZiSheng001/p/13581390.html](https://www.cnblogs.com/ZhangZiSheng001/p/1358139