任何新工具的出現(xiàn),都是為了解決某個(gè)具體問題而誕生的,否則就沒有存在的必要了
本文章將會(huì)概述一下 基準(zhǔn)測試的概念 、StopWatch的基本使用、 JMH的基本使用與用戶JMH中常用注解概述。
1 引言
JMH 全稱 JAVA Microbenchmark Harness ,Microbenchmark 可解析為 短語 micro-benchmark 測試,Microbenchmark也可解析為 micro(基本的)benchmark(標(biāo)準(zhǔn)檢查程序) 。
JMH 是由 Java Jvm 虛擬機(jī)團(tuán)隊(duì)開發(fā) ,在Jvm 對(duì) Java 文件的編譯階段、類的加載階段、運(yùn)行階段者有持續(xù)的不同程度的優(yōu)化,JMH的誕生就是為了讓 Java 開發(fā)者能夠了解到自己所編寫的代碼運(yùn)行的情況,以及性能方面的情況。
1.1 基準(zhǔn)測試 ?
基準(zhǔn)測試是指通過設(shè)計(jì)科學(xué)的測試方法、測試工具和測試系統(tǒng),實(shí)現(xiàn)對(duì)一類測試對(duì)象的某項(xiàng)性能指標(biāo)進(jìn)行定量的和可對(duì)比的測試。
1.2 使用 StopWatch 來進(jìn)行測試時(shí)間計(jì)算
一個(gè)常見的問題 就是 我們會(huì)說 ArrayList 比 LinkedList 性能好點(diǎn),那么我們總會(huì)要想方法去測試一下,如添加 1000 0000 條數(shù)據(jù),看誰消耗的時(shí)間少, StopWatch 用來記錄這個(gè)時(shí)間差并可生成對(duì)比,如下代碼清單 1-1 中所示的測試用例中,分別向 ArrayList 、LinkedList 中添加了 1000 0000 條數(shù)據(jù),然后通過 StopWatch 來生成時(shí)間消耗對(duì)比:
///代碼清單 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(); //開始計(jì)時(shí)
stopWatch.start("arrayList 測試");
for (int i = 0; i < 10000000; i++) {
arrayList.add("測試數(shù)據(jù)");
} ///停止計(jì)時(shí)
stopWatch.stop(); //測試 LinkedList
List<String> linkedList = new LinkedList<>(); //開始計(jì)時(shí)
stopWatch.start("linkedList 測試");
for (int i = 0; i < 10000000; i++) {
linkedList.add("測試數(shù)據(jù)");
} ///停止計(jì)時(shí)
stopWatch.stop(); LOG.info("arrayList 消耗的總時(shí)間 " + stopWatch.prettyPrint());
LOG.info("arrayList 消耗的總時(shí)間 " + stopWatch.getTotalTimeMillis());
} }
然后執(zhí)行單元測試后生成 如下結(jié)果:
很明顯 對(duì)于add方法來講,ArrayList 的性能要比 LinkedList 的性能要好點(diǎn)。
在這里只是一個(gè)粗糙的測試方法,因?yàn)椋?/p>
- 1. 使用到的 StopWatch ,在其內(nèi)部也會(huì)記錄方法的開始的納秒數(shù),這種操作也會(huì)消耗一定的CPU時(shí)間。
2.JVM 在運(yùn)行時(shí)對(duì) for 循環(huán)也有優(yōu)化,這樣就會(huì)導(dǎo)致測試時(shí)間包含了一部分JVM性能優(yōu)化的執(zhí)行時(shí)間3.前后運(yùn)行的 JVM 環(huán)境并不完全相同
所以為了能更嚴(yán)謹(jǐn)?shù)膩磉M(jìn)行測試, JMH 就出現(xiàn)了。
2 JMH 基本使用
2.1 集成
JMH是 JDK9自帶的,如果你是 JDK9 之前的版本也可以通過導(dǎo)入 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 進(jìn)行測試
///代碼清單 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 進(jìn)行 Benchmark 時(shí)所使用的模式
//BenchmarkMode的value是一個(gè)數(shù)組,可以把幾種Mode集合在一起執(zhí)行,還可以設(shè)置為Mode.Al
@BenchmarkMode(Mode.AverageTime)
//benchmark 結(jié)果所使用的時(shí)間單位
//使用java.util.concurrent.TimeUnit中的標(biāo)準(zhǔn)時(shí)間單位
// 微秒
@OutputTimeUnit(TimeUnit.MICROSECONDS)
///JMH測試類必須使用@State注解,
// State定義了一個(gè)類實(shí)例的生命周期,
// 可以類比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("測試數(shù)據(jù)");
}
}
@Benchmark
public void testLinkedList() {
//測試 LinkedList
List<String> linkedList = new LinkedList<>();
for (int i = 0; i < 10000000; i++) {
linkedList.add("測試數(shù)據(jù)");
}
}
}
然后運(yùn)行main 方法后控制臺(tái)日志會(huì)輸出很長的日志信息,在這里是執(zhí)行了testArrayList 與testLinkedList兩個(gè)方法的基準(zhǔn)測試,每個(gè)方法都會(huì)對(duì)應(yīng)一段日志信息,小編在這里將testArrayList 方法 日志信息拆分成兩段如下:
第一段包括 JVM 的啟動(dòng)參數(shù)配置信息 以及 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 可譯為 預(yù)熱的意思,在 JMH 中,Warmup 所做 的事情就是在基準(zhǔn)測試代碼正式執(zhí)行測量(度量)前,對(duì)其進(jìn)行預(yù)熱,如 JVM運(yùn)行器的編譯、JIT 的優(yōu)化等等
第二段 就是JMH 對(duì) 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)
然后 對(duì)于 testLinkedList 方法也會(huì)有 相同類似的日志信息只不過是輸出的數(shù)據(jù)不一樣,當(dāng)兩個(gè)方法執(zhí)行基準(zhǔn)測試完成后 最后會(huì)有對(duì)比信息日志如下:
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 對(duì)比
我期望與 代碼清單 1-1 所使用的 StopWatch 計(jì)時(shí)對(duì)比一下時(shí)時(shí)間 ,StopWatch 中輸出的是納秒,對(duì)應(yīng)的是 TimeUnit.NANOSECONDS ,所以我需要將 @OutputTimeUnit 配置的單位修改,以使用 JMH 度量后的時(shí)間單位輸出為納秒
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class DemoApplicationTestJMH { ...}
再次運(yùn)行度量測試 最終日志如下:
# 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 參數(shù)概述
3.1 @BenchmarkMode
對(duì)應(yīng)Mode選項(xiàng),可用于類或者方法上, 需要注意的是,這個(gè)注解的value是一個(gè)數(shù)組,可以把幾種Mode集合在一起執(zhí)行,還可以設(shè)置為Mode.All,即全部執(zhí)行一遍
Mode 表示 JMH 進(jìn)行 Benchmark 時(shí)所使用的模式。通常是測量的維度不同,或是測量的方式不同。目前 JMH 共有四種模式:
- Throughput: 整體吞吐量,例如“1秒內(nèi)可以執(zhí)行多少次調(diào)用”。
- AverageTime: 調(diào)用的平均時(shí)間,例如“每次調(diào)用平均耗時(shí)xxx毫秒”。
- SampleTime: 隨機(jī)取樣,最后輸出取樣結(jié)果的分布,例如“99%的調(diào)用在xxx毫秒以內(nèi),99.99%的調(diào)用在xxx毫秒以內(nèi)”
- SingleShotTime: 以上模式都是默認(rèn)一次 iteration 是 1s,唯有 SingleShotTime 是只運(yùn)行一次。往往同時(shí)把 warmup 次數(shù)設(shè)為0,用于測試?yán)鋯?dòng)時(shí)的性能。
3.2 Iteration 與 Warmup
Iteration 是 JMH 進(jìn)行測試的最小單位。在大部分模式下,一次 iteration 代表的是一秒,JMH 會(huì)在這一秒內(nèi)不斷調(diào)用需要 benchmark 的方法,然后根據(jù)模式對(duì)其采樣,計(jì)算吞吐量,計(jì)算平均執(zhí)行時(shí)間等。
Warmup 是指在實(shí)際進(jìn)行 benchmark 前先進(jìn)行預(yù)熱的行為。為什么需要預(yù)熱?因?yàn)?JVM 的 JIT 機(jī)制的存在,如果某個(gè)函數(shù)被調(diào)用多次之后,JVM 會(huì)嘗試將其編譯成為機(jī)器碼從而提高執(zhí)行速度。為了讓 benchmark 的結(jié)果更加接近真實(shí)情況就需要進(jìn)行預(yù)熱。
3.3 @State
類注解,JMH測試類必須使用@State注解,State定義了一個(gè)類實(shí)例的生命周期,可以類比Spring Bean的Scope。
由于JMH允許多線程同時(shí)執(zhí)行測試,不同的選項(xiàng)含義如下:
- Scope.Thread:默認(rèn)的State,每個(gè)測試線程分配一個(gè)實(shí)例;
- Scope.Benchmark:所有測試線程共享一個(gè)實(shí)例,用于測試有狀態(tài)實(shí)例在多線程共享下的性能;
- Scope.Group:每個(gè)線程組共享一個(gè)實(shí)例; ##### 3.4 @OutputTimeUnit 用來配置benchmark 結(jié)果所使用的時(shí)間單位,可用于類或者方法注解,使用java.util.concurrent.TimeUnit中的標(biāo)準(zhǔn)時(shí)間單位。
TimeUnit.DAYS //天
TimeUnit.HOURS //小時(shí)
TimeUnit.MINUTES //分鐘
TimeUnit.SECONDS //秒
TimeUnit.MILLISECONDS //毫秒
TimeUnit.NANOSECONDS //毫微秒 納秒
TimeUnit.MICROSECONDS //微秒
3.5 其他
@Benchmark
方法注解,表示該方法是需要進(jìn)行 benchmark 的對(duì)象。
@Setup
方法注解,會(huì)在執(zhí)行 benchmark 之前被執(zhí)行,正如其名,主要用于初始化。
@TearDown
方法注解,與@Setup 相對(duì)的,會(huì)在所有 benchmark 執(zhí)行結(jié)束以后執(zhí)行,主要用于資源的回收等。
@Param
成員注解,可以用來指定某項(xiàng)參數(shù)的多種情況。特別適合用來測試一個(gè)函數(shù)在不同的參數(shù)輸入的情況下的性能。@Param注解接收一個(gè)String數(shù)組,在@setup方法執(zhí)行前轉(zhuǎn)化為為對(duì)應(yīng)的數(shù)據(jù)類型。多個(gè)@Param注解的成員之間是乘積關(guān)系,譬如有兩個(gè)用@Param注解的字段,第一個(gè)有5個(gè)值,第二個(gè)字段有2個(gè)值,那么每個(gè)測試方法會(huì)跑5*2=10次。






