別再寫 main 方法測試了,太 Low!這才是專業(yè) Java 測試方法!
1
"If you cannot measure it, you cannot improve it".
在日常開發(fā)中,我們對一些代碼的調(diào)用或者工具的使用會存在多種選擇方式,在不確定他們性能的時候,我們首先想要做的就是去測量它。大多數(shù)時候,我們會簡單的采用多次計(jì)數(shù)的方式來測量,來看這個方法的總耗時。
但是,如果熟悉JVM類加載機(jī)制的話,應(yīng)該知道JVM默認(rèn)的執(zhí)行模式是JIT編譯與解釋混合執(zhí)行。JVM通過熱點(diǎn)代碼統(tǒng)計(jì)分析,識別高頻方法的調(diào)用、循環(huán)體、公共模塊等,基于JIT動態(tài)編譯技術(shù),會將熱點(diǎn)代碼轉(zhuǎn)換成機(jī)器碼,直接交給CPU執(zhí)行。

也就是說,JVM會不斷的進(jìn)行編譯優(yōu)化,這就使得很難確定重復(fù)多少次才能得到一個穩(wěn)定的測試結(jié)果?所以,很多有經(jīng)驗(yàn)的同學(xué)會在測試代碼前寫一段預(yù)熱的邏輯。
JMH,全稱 Java Microbenchmark Harness (微基準(zhǔn)測試框架),是專門用于Java代碼微基準(zhǔn)測試的一套測試工具API,是由 OpenJDK/Oracle 官方發(fā)布的工具。何謂 Micro Benchmark 呢?簡單地說就是在 method 層面上的 benchmark,精度可以精確到微秒級。
Java的基準(zhǔn)測試需要注意的幾個點(diǎn):
測試前需要預(yù)熱。
防止無用代碼進(jìn)入測試方法中。
并發(fā)測試。
測試結(jié)果呈現(xiàn)。
JMH的使用場景:
定量分析某個熱點(diǎn)函數(shù)的優(yōu)化效果
想定量地知道某個函數(shù)需要執(zhí)行多長時間,以及執(zhí)行時間和輸入變量的相關(guān)性
對比一個函數(shù)的多種實(shí)現(xiàn)方式
2
這里先演示一個DEMO,讓不了解JMH的同學(xué)能夠快速掌握這個工具的大概用法。
1. 測試項(xiàng)目構(gòu)建
JMH是內(nèi)置Java9及之后的版本。這里是以Java8進(jìn)行說明。
為了方便,這里直接介紹使用maven構(gòu)建JMH測試項(xiàng)目的方式。
第一種是使用命令行構(gòu)建,在指定目錄下執(zhí)行以下命令:
$ mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0對應(yīng)目錄下會出現(xiàn)一個test項(xiàng)目,打開項(xiàng)目后我們會看到這樣的項(xiàng)目結(jié)構(gòu)。

第二種方式就是直接在現(xiàn)有的maven項(xiàng)目中添加jmh-core和jmh-generator-annprocess的依賴來集成JMH。
<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>2. 編寫性能測試
這里我以測試LinkedList 通過index 方式迭代和foreach 方式迭代的性能差距為例子,編寫測試類,涉及到的注解在之后會講解,
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.SECONDS)
@Threads(Threads.MAX)
public class LinkedListIterationBenchMark {
private static final int SIZE = 10000;
private List<String> list = new LinkedList<>();
@Setup
public void setUp() {
for (int i = 0; i < SIZE; i++) {
list.add(String.valueOf(i));
}
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void forIndexIterate() {
for (int i = 0; i < list.size(); i++) {
list.get(i);
System.out.print("");
}
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void forEachIterate() {
for (String s : list) {
System.out.print("");
}
}
}3. 執(zhí)行測試
運(yùn)行 JMH 基準(zhǔn)測試有兩種方式,一個是生產(chǎn)jar文件運(yùn)行,另一個是直接寫main函數(shù)或者放在單元測試中執(zhí)行。
生成jar文件的形式主要是針對一些比較大的測試,可能對機(jī)器性能或者真實(shí)環(huán)境模擬有一些需求,需要將測試方法寫好了放在linux環(huán)境執(zhí)行。具體命令如下
$ mvn clean install
$ java -jar target/benchmarks.jar我們?nèi)粘V杏龅降囊话闶且恍┬y試,比如我上面寫的例子,直接在IDE中跑就好了。啟動方式如下:
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(LinkedListIterationBenchMark.class.getSimpleName())
.forks(1)
.warmupIterations(2)
.measurementIterations(2)
.output("E:/Benchmark.log")
.build();
new Runner(opt).run();
}4. 報告結(jié)果
輸出結(jié)果如下
最后的結(jié)果:
Benchmark Mode Cnt Score Error Units
LinkedListIterationBenchMark.forEachIterate thrpt 2 1192.380 ops/s
LinkedListIterationBenchMark.forIndexIterate thrpt 2 206.866 ops/s整個過程:
# Detecting actual CPU count: 12 detected
# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 12 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forEachIterate
# Run progress: 0.00% complete, ETA 00:01:20
# Fork: 1 of 1
# Warmup Iteration 1: 1189.267 ops/s
# Warmup Iteration 2: 1197.321 ops/s
Iteration 1: 1193.062 ops/s
Iteration 2: 1191.698 ops/s
Result "org.sample.jmh.LinkedListIterationBenchMark.forEachIterate":
1192.380 ops/s
# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 12 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate
# Run progress: 50.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration 1: 205.676 ops/s
# Warmup Iteration 2: 206.512 ops/s
Iteration 1: 206.542 ops/s
Iteration 2: 207.189 ops/s
Result "org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate":
206.866 ops/s
# Run complete. Total time: 00:01:21
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
LinkedListIterationBenchMark.forEachIterate thrpt 2 1192.380 ops/s
LinkedListIterationBenchMark.forIndexIterate thrpt 2 206.866 ops/s3
下面我們來詳細(xì)介紹一下相關(guān)的注解
@BenchmarkMode
微基準(zhǔn)測試類型。JMH 提供了以下幾種類型進(jìn)行支持:

可以注釋在方法級別,也可以注釋在類級別,
@BenchmarkMode(Mode.All)
public class LinkedListIterationBenchMark {
...
}
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
public void m() {
...
}@Warmup
這個單詞的意思就是預(yù)熱,iterations = 3就是指預(yù)熱輪數(shù)。
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Warmup(iterations = 3)
public void m() {
...
}@Measurement
正式度量計(jì)算的輪數(shù)。
iterations 進(jìn)行測試的輪次
time 每輪進(jìn)行的時長
timeUnit時長單位
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Measurement(iterations = 3)
public void m() {
...
}@Threads
每個進(jìn)程中的測試線程。
@Threads(Threads.MAX)
public class LinkedListIterationBenchMark {
...
}@Fork
進(jìn)行 fork 的次數(shù)。如果 fork 數(shù)是3的話,則 JMH 會 fork 出3個進(jìn)程來進(jìn)行測試。
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Fork(value = 3)
public void m() {
...
}@OutputTimeUnit
基準(zhǔn)測試結(jié)果的時間類型。一般選擇秒、毫秒、微秒。
@OutputTimeUnit(TimeUnit.SECONDS)
public class LinkedListIterationBenchMark {
...
}@Benchmark
方法級注解,表示該方法是需要進(jìn)行 benchmark 的對象,用法和 JUnit 的 @Test 類似。
@Param
屬性級注解,@Param 可以用來指定某項(xiàng)參數(shù)的多種情況。特別適合用來測試一個函數(shù)在不同的參數(shù)輸入的情況下的性能。
@Setup
方法級注解,這個注解的作用就是我們需要在測試之前進(jìn)行一些準(zhǔn)備工作,比如對一些數(shù)據(jù)的初始化之類的。
@TearDown
方法級注解,這個注解的作用就是我們需要在測試之后進(jìn)行一些結(jié)束工作,比如關(guān)閉線程池,數(shù)據(jù)庫連接等的,主要用于資源的回收等。
@State
當(dāng)使用@Setup參數(shù)的時候,必須在類上加這個參數(shù),不然會提示無法運(yùn)行。
就比如我上面的例子中,就必須設(shè)置state。
State 用于聲明某個類是一個“狀態(tài)”,然后接受一個 Scope 參數(shù)用來表示該狀態(tài)的共享范圍。因?yàn)楹芏?benchmark 會需要一些表示狀態(tài)的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數(shù)里。Scope 主要分為三種。
Thread: 該狀態(tài)為每個線程獨(dú)享。
Group: 該狀態(tài)為同一個組里面所有線程共享。
Benchmark: 該狀態(tài)在所有線程間共享。
啟動方法
在啟動方法中,可以直接指定上述說到的一些參數(shù),并且能將測試結(jié)果輸出到指定文件中
/**
* 僅限于IDE中運(yùn)行
* 命令行模式 則是 build 然后 java -jar 啟動
*
* 1. 這是benchmark 啟動的入口
* 2. 這里同時還完成了JMH測試的一些配置工作
* 3. 默認(rèn)場景下,JMH會去找尋標(biāo)注了@Benchmark的方法,可以通過include和exclude兩個方法來完成包含以及排除的語義
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
// 包含語義
// 可以用方法名,也可以用XXX.class.getSimpleName()
.include("Helloworld")
// 排除語義
.exclude("Pref")
// 預(yù)熱10輪
.warmupIterations(10)
// 代表正式計(jì)量測試做10輪,
// 而每次都是先執(zhí)行完預(yù)熱再執(zhí)行正式計(jì)量,
// 內(nèi)容都是調(diào)用標(biāo)注了@Benchmark的代碼。
.measurementIterations(10)
// forks(3)指的是做3輪測試,
// 因?yàn)橐淮螠y試無法有效的代表結(jié)果,
// 所以通過3輪測試較為全面的測試,
// 而每一輪都是先預(yù)熱,再正式計(jì)量。
.forks(3)
.output("E:/Benchmark.log")
.build();
new Runner(opt).run();
}4
基于JMH可以對很多工具和框架進(jìn)行測試,比如日志框架性能對比、BeanCopy性能對比 等,更多的example可以參考官方給出的JMH samples
來源:juejin.cn/post/6844903936869007368
加小編微信,回復(fù) 40 白嫖40套 java/spring/kafka/redis/netty 教程/代碼/視頻 等
掃二維碼,加我微信,回復(fù):40
注意,不要亂回復(fù) 沒錯,不是機(jī)器人 記得一定要等待,等待才有好東西
