任何新工具的出現,都是為了解決某個具體問題而誕生的,否則就沒有存在的必要了
本文章將會概述一下 基準測試的概念 、StopWatch的基本使用、 JMH的基本使用與用戶JMH中常用注解概述。
1 引言
JMH 全稱 JAVA Microbenchmark Harness ,Microbenchmark 可解析為 短語 micro-benchmark 測試,Microbenchmark也可解析為 micro(基本的)benchmark(標準檢查程序) 。
JMH 是由 Java Jvm 虛擬機團隊開發 ,在Jvm 對 Java 文件的編譯階段、類的加載階段、運行階段者有持續的不同程度的優化,JMH的誕生就是為了讓 Java 開發者能夠了解到自己所編寫的代碼運行的情況,以及性能方面的情況。
1.1 基準測試 ?
基準測試是指通過設計科學的測試方法、測試工具和測試系統,實現對一類測試對象的某項性能指標進行定量的和可對比的測試。
1.2 使用 StopWatch 來進行測試時間計算
一個常見的問題 就是 我們會說 ArrayList 比 LinkedList 性能好點,那么我們總會要想方法去測試一下,如添加 1000 0000 條數據,看誰消耗的時間少, StopWatch 用來記錄這個時間差并可生成對比,如下代碼清單 1-1 中所示的測試用例中,分別向 ArrayList 、LinkedList 中添加了 1000 0000 條數據,然后通過 StopWatch 來生成時間消耗對比:
///代碼清單 1-1
package com.example.demo;import org.junit.jupiter.api.Test;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.util.StopWatch;import java.util.ArrayList;import java.util.LinkedList;import java.util.List;@SpringBootTestclass DemoApplicationTest2 { private static final Logger LOG = LoggerFactory.getLogger(DemoApplicationTest2.class);
@Test void testArrayAndLinkedList() { List<String> arrayList = new ArrayList<>(); StopWatch stopWatch = new StopWatch(); //開始計時
stopWatch.start("arrayList 測試");
for (int i = 0; i < 10000000; i++) {
arrayList.add("測試數據");
} ///停止計時
stopWatch.stop(); //測試 LinkedList
List<String> linkedList = new LinkedList<>(); //開始計時
stopWatch.start("linkedList 測試");
for (int i = 0; i < 10000000; i++) {
linkedList.add("測試數據");
} ///停止計時
stopWatch.stop(); LOG.info("arrayList 消耗的總時間 " + stopWatch.prettyPrint());
LOG.info("arrayList 消耗的總時間 " + stopWatch.getTotalTimeMillis());
} }
然后執行單元測試后生成 如下結果:
很明顯 對于add方法來講,ArrayList 的性能要比 LinkedList 的性能要好點。
在這里只是一個粗糙的測試方法,因為:
- 1. 使用到的 StopWatch ,在其內部也會記錄方法的開始的納秒數,這種操作也會消耗一定的CPU時間。
2.JVM 在運行時對 for 循環也有優化,這樣就會導致測試時間包含了一部分JVM性能優化的執行時間3.前后運行的 JVM 環境并不完全相同
所以為了能更嚴謹的來進行測試, JMH 就出現了。
2 JMH 基本使用
2.1 集成
JMH是 JDK9自帶的,如果你是 JDK9 之前的版本也可以通過導入 openjdk
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
</dependency>
2.2 使用 JMH 進行測試
///代碼清單 2-1
import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;import java.util.ArrayList;import java.util.LinkedList;import java.util.List;import java.util.concurrent.TimeUnit;//Mode 表示 JMH 進行 Benchmark 時所使用的模式
//BenchmarkMode的value是一個數組,可以把幾種Mode集合在一起執行,還可以設置為Mode.Al
@BenchmarkMode(Mode.AverageTime)
//benchmark 結果所使用的時間單位
//使用java.util.concurrent.TimeUnit中的標準時間單位
// 微秒
@OutputTimeUnit(TimeUnit.MICROSECONDS)
///JMH測試類必須使用@State注解,
// State定義了一個類實例的生命周期,
// 可以類比Spring Bean的Scope
@State(Scope.Thread)
public class DemoApplicationTestJMH {
public static void main(String[] args) throws Exception {
String name = DemoApplicationTestJMH.class.getName();
Options options = new OptionsBuilder()
.include(name )
.forks(1)
.measurementIterations(3)
.warmupIterations(3)
.build();
new Runner(options).run();
}
@Benchmark
public void testArrayList() {
List<String> arrayList = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
arrayList.add("測試數據");
}
}
@Benchmark
public void testLinkedList() {
//測試 LinkedList
List<String> linkedList = new LinkedList<>();
for (int i = 0; i < 10000000; i++) {
linkedList.add("測試數據");
}
}
}
然后運行main 方法后控制臺日志會輸出很長的日志信息,在這里是執行了testArrayList 與testLinkedList兩個方法的基準測試,每個方法都會對應一段日志信息,小編在這里將testArrayList 方法 日志信息拆分成兩段如下:
第一段包括 JVM 的啟動參數配置信息 以及 JMH 的基本配置
/Library/Java/JavaVirtualmachines/jdk1.8.0_74.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=65371: ... 省略路徑
IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath
# JMH version: 1.19
# VM version: JDK 1.8.0_74, VM 25.74-b02
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_74.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=65371:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 3 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.example.demo.DemoApplicationTestJMH.testArrayList
# Run progress: 0.00% complete, ETA 00:00:12
# Fork: 1 of 1
objc[56915]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_74.jdk/Contents/Home/jre/bin/java (0x10d1a44c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_74.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x10d1e94e0). One of the two will be used. Which one is undefined.
# Warmup Iteration 1: 101153.611 us/op
# Warmup Iteration 2: 76302.787 us/op
# Warmup Iteration 3: 54296.903 us/op
Iteration 1: 57062.920 us/op
Iteration 2: 65024.286 us/op
Iteration 3: 56325.284 us/op
分析如下圖
Warmup 可譯為 預熱的意思,在 JMH 中,Warmup 所做 的事情就是在基準測試代碼正式執行測量(度量)前,對其進行預熱,如 JVM運行器的編譯、JIT 的優化等等
第二段 就是JMH 對 testArrayList 方法的測試輸出信息了
Result "com.example.demo.DemoApplicationTestJMH.testArrayList":
59470.830 ±(99.9%) 87999.595 us/op [Average]
(min, avg, max) = (56325.284, 59470.830, 65024.286), stdev = 4823.555
CI (99.9%): [≈ 0, 147470.424] (assumes normal distribution)
然后 對于 testLinkedList 方法也會有 相同類似的日志信息只不過是輸出的數據不一樣,當兩個方法執行基準測試完成后 最后會有對比信息日志如下:
Result "com.example.demo.DemoApplicationTestJMH.testLinkedList":
206870.042 ±(99.9%) 2571061.260 us/op [Average]
(min, avg, max) = (104680.987, 206870.042, 367640.921), stdev = 140928.543
CI (99.9%): [≈ 0, 2777931.302] (assumes normal distribution)
# Run complete. Total time: 00:00:16
Benchmark Mode Cnt Score Error Units
DemoApplicationTestJMH.testArrayList avgt 3 59470.830 ± 87999.595 us/op
DemoApplicationTestJMH.testLinkedList avgt 3 206870.042 ± 2571061.260 us/op
2.3 對比
我期望與 代碼清單 1-1 所使用的 StopWatch 計時對比一下時時間 ,StopWatch 中輸出的是納秒,對應的是 TimeUnit.NANOSECONDS ,所以我需要將 @OutputTimeUnit 配置的單位修改,以使用 JMH 度量后的時間單位輸出為納秒
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class DemoApplicationTestJMH { ...}
再次運行度量測試 最終日志如下:
# Run complete. Total time: 00:00:16
Benchmark Mode Cnt Score Error Units
DemoApplicationTestJMH.testArrayList avgt 3 57019569.485 ± 88169595.613 ns/op
DemoApplicationTestJMH.testLinkedList avgt 3 368470033.556 ± 535285211.800 ns/op
Process finished with exit code 0
代碼清單 1-1 所使用的 StopWatch 日志如下:
---------------------------------------------
ns % Task name
---------------------------------------------
177413718 044% arrayList 測試
227298885 056% linkedList 測試
3 參數概述
3.1 @BenchmarkMode
對應Mode選項,可用于類或者方法上, 需要注意的是,這個注解的value是一個數組,可以把幾種Mode集合在一起執行,還可以設置為Mode.All,即全部執行一遍
Mode 表示 JMH 進行 Benchmark 時所使用的模式。通常是測量的維度不同,或是測量的方式不同。目前 JMH 共有四種模式:
- Throughput: 整體吞吐量,例如“1秒內可以執行多少次調用”。
- AverageTime: 調用的平均時間,例如“每次調用平均耗時xxx毫秒”。
- SampleTime: 隨機取樣,最后輸出取樣結果的分布,例如“99%的調用在xxx毫秒以內,99.99%的調用在xxx毫秒以內”
- SingleShotTime: 以上模式都是默認一次 iteration 是 1s,唯有 SingleShotTime 是只運行一次。往往同時把 warmup 次數設為0,用于測試冷啟動時的性能。
3.2 Iteration 與 Warmup
Iteration 是 JMH 進行測試的最小單位。在大部分模式下,一次 iteration 代表的是一秒,JMH 會在這一秒內不斷調用需要 benchmark 的方法,然后根據模式對其采樣,計算吞吐量,計算平均執行時間等。
Warmup 是指在實際進行 benchmark 前先進行預熱的行為。為什么需要預熱?因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之后,JVM 會嘗試將其編譯成為機器碼從而提高執行速度。為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。
3.3 @State
類注解,JMH測試類必須使用@State注解,State定義了一個類實例的生命周期,可以類比Spring Bean的Scope。
由于JMH允許多線程同時執行測試,不同的選項含義如下:
- Scope.Thread:默認的State,每個測試線程分配一個實例;
- Scope.Benchmark:所有測試線程共享一個實例,用于測試有狀態實例在多線程共享下的性能;
- Scope.Group:每個線程組共享一個實例; ##### 3.4 @OutputTimeUnit 用來配置benchmark 結果所使用的時間單位,可用于類或者方法注解,使用java.util.concurrent.TimeUnit中的標準時間單位。
TimeUnit.DAYS //天
TimeUnit.HOURS //小時
TimeUnit.MINUTES //分鐘
TimeUnit.SECONDS //秒
TimeUnit.MILLISECONDS //毫秒
TimeUnit.NANOSECONDS //毫微秒 納秒
TimeUnit.MICROSECONDS //微秒
3.5 其他
@Benchmark
方法注解,表示該方法是需要進行 benchmark 的對象。
@Setup
方法注解,會在執行 benchmark 之前被執行,正如其名,主要用于初始化。
@TearDown
方法注解,與@Setup 相對的,會在所有 benchmark 執行結束以后執行,主要用于資源的回收等。
@Param
成員注解,可以用來指定某項參數的多種情況。特別適合用來測試一個函數在不同的參數輸入的情況下的性能。@Param注解接收一個String數組,在@setup方法執行前轉化為為對應的數據類型。多個@Param注解的成員之間是乘積關系,譬如有兩個用@Param注解的字段,第一個有5個值,第二個字段有2個值,那么每個測試方法會跑5*2=10次。