1. 程式人生 > >JMH--一款由OpenJDK開發的基準測試工具

JMH--一款由OpenJDK開發的基準測試工具

# 什麼是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