JMH - Java 微基準測試工具
點擊關注公眾號,Java干貨及時送達??
來源:https://juejin.cn/post/6844903936869007368前言
"If you cannot measure it, you cannot improve it".
在日常開發(fā)中,我們對一些代碼的調(diào)用或者工具的使用會存在多種選擇方式,在不確定他們性能的時候,我們首先想要做的就是去測量它。大多數(shù)時候,我們會簡單的采用多次計數(shù)的方式來測量,來看這個方法的總耗時。
但是,如果熟悉JVM類加載機制的話,應該知道JVM默認的執(zhí)行模式是JIT編譯與解釋混合執(zhí)行。JVM通過熱點代碼統(tǒng)計分析,識別高頻方法的調(diào)用、循環(huán)體、公共模塊等,基于JIT動態(tài)編譯技術(shù),會將熱點代碼轉(zhuǎn)換成機器碼,直接交給CPU執(zhí)行。

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

第二種方式就是直接在現(xiàn)有的maven項目中添加jmh-core和jmh-generator-annprocess的依賴來集成JMH。
<dependency>
????<groupId>org.openjdk.jmhgroupId>
????<artifactId>jmh-coreartifactId>
????<version>${jmh.version}version>
dependency>
<dependency>
????<groupId>org.openjdk.jmhgroupId>
????<artifactId>jmh-generator-annprocessartifactId>
????<version>${jmh.version}version>
????<scope>providedscope>
dependency>
2. 編寫性能測試
這里我以測試LinkedList 通過index 方式迭代和foreach 方式迭代的性能差距為例子,編寫測試類,涉及到的注解在之后會講解,
/**
?*?@author?Richard_yyf
?*?@version?1.0?2019/8/27
?*/
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.SECONDS)
@Threads(Threads.MAX)
public?class?LinkedListIterationBenchMark?{
?private?static?final?int?SIZE?=?10000;
????private?List?list?=?new?LinkedList<>();
????
????@Setup
????public?void?setUp()?{
????????for?(int?i?=?0;?i?????????????list.add(String.valueOf(i));
????????}
????}
????@Benchmark
????@BenchmarkMode(Mode.Throughput)
????public?void?forIndexIterate()?{
????????for?(int?i?=?0;?i?????????????list.get(i);
????????????System.out.print("");
????????}
????}
????@Benchmark
????@BenchmarkMode(Mode.Throughput)
????public?void?forEachIterate()?{
????????for?(String?s?:?list)?{
????????????System.out.print("");
????????}
????}
}
3. 執(zhí)行測試
運行 JMH 基準測試有兩種方式,一個是生產(chǎn)jar文件運行,另一個是直接寫main函數(shù)或者放在單元測試中執(zhí)行。
生成jar文件的形式主要是針對一些比較大的測試,可能對機器性能或者真實環(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/s
注解介紹
下面我們來詳細介紹一下相關的注解,
@BenchmarkMode
微基準測試類型。JMH 提供了以下幾種類型進行支持:
| 類型 | 描述 |
|---|---|
| Throughput | 每段時間執(zhí)行的次數(shù),一般是秒 |
| AverageTime | 平均時間,每次操作的平均耗時 |
| SampleTime | 在測試中,隨機進行采樣執(zhí)行的時間 |
| SingleShotTime | 在每次執(zhí)行中計算耗時 |
| All | 所有模式 |
可以注釋在方法級別,也可以注釋在類級別,
@BenchmarkMode(Mode.All)
public?class?LinkedListIterationBenchMark?{
?...
}
@Benchmark
@BenchmarkMode({Mode.Throughput,?Mode.SingleShotTime})
public?void?m()?{
?...
}
@Warmup
這個單詞的意思就是預熱,iterations = 3就是指預熱輪數(shù)。
@Benchmark
@BenchmarkMode({Mode.Throughput,?Mode.SingleShotTime})
@Warmup(iterations?=?3)
public?void?m()?{
?...
}
@Measurement
正式度量計算的輪數(shù)。
iterations進行測試的輪次time每輪進行的時長timeUnit時長單位
@Benchmark
@BenchmarkMode({Mode.Throughput,?Mode.SingleShotTime})
@Measurement(iterations?=?3)
public?void?m()?{
?...
}
@Threads
每個進程中的測試線程。
@Threads(Threads.MAX)
public?class?LinkedListIterationBenchMark?{
?...
}
@Fork
進行 fork 的次數(shù)。如果 fork 數(shù)是3的話,則 JMH 會 fork 出3個進程來進行測試。
@Benchmark
@BenchmarkMode({Mode.Throughput,?Mode.SingleShotTime})
@Fork(value?=?3)
public?void?m()?{
?...
}
@OutputTimeUnit
基準測試結(jié)果的時間類型。一般選擇秒、毫秒、微秒。
@OutputTimeUnit(TimeUnit.SECONDS)
public?class?LinkedListIterationBenchMark?{
?...
}
@Benchmark
方法級注解,表示該方法是需要進行 benchmark 的對象,用法和 JUnit 的 @Test 類似。
@Param
屬性級注解,@Param 可以用來指定某項參數(shù)的多種情況。特別適合用來測試一個函數(shù)在不同的參數(shù)輸入的情況下的性能。
@Setup
方法級注解,這個注解的作用就是我們需要在測試之前進行一些準備工作,比如對一些數(shù)據(jù)的初始化之類的。
@TearDown
方法級注解,這個注解的作用就是我們需要在測試之后進行一些結(jié)束工作,比如關閉線程池,數(shù)據(jù)庫連接等的,主要用于資源的回收等。
@State
當使用@Setup參數(shù)的時候,必須在類上加這個參數(shù),不然會提示無法運行。
就比如我上面的例子中,就必須設置state。
State 用于聲明某個類是一個“狀態(tài)”,然后接受一個 Scope 參數(shù)用來表示該狀態(tài)的共享范圍。因為很多 benchmark 會需要一些表示狀態(tài)的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數(shù)里。Scope 主要分為三種。
Thread: 該狀態(tài)為每個線程獨享。 Group: 該狀態(tài)為同一個組里面所有線程共享。 Benchmark: 該狀態(tài)在所有線程間共享。
啟動方法
在啟動方法中,可以直接指定上述說到的一些參數(shù),并且能將測試結(jié)果輸出到指定文件中,
/**
?*?僅限于IDE中運行
?*?命令行模式?則是?build?然后?java?-jar?啟動
?*
?*?1.?這是benchmark?啟動的入口
?*?2.?這里同時還完成了JMH測試的一些配置工作
?*?3.?默認場景下,JMH會去找尋標注了@Benchmark的方法,可以通過include和exclude兩個方法來完成包含以及排除的語義
?*/
public?static?void?main(String[]?args)?throws?RunnerException?{
????Options?opt?=?new?OptionsBuilder()
????????????//?包含語義
????????????//?可以用方法名,也可以用XXX.class.getSimpleName()
????????????.include("Helloworld")
????????????//?排除語義
????????????.exclude("Pref")
????????????//?預熱10輪
????????????.warmupIterations(10)
????????????//?代表正式計量測試做10輪,
????????????//?而每次都是先執(zhí)行完預熱再執(zhí)行正式計量,
????????????//?內(nèi)容都是調(diào)用標注了@Benchmark的代碼。
????????????.measurementIterations(10)
????????????//??forks(3)指的是做3輪測試,
????????????//?因為一次測試無法有效的代表結(jié)果,
????????????//?所以通過3輪測試較為全面的測試,
????????????//?而每一輪都是先預熱,再正式計量。
????????????.forks(3)
?????????.output("E:/Benchmark.log")
????????????.build();
????new?Runner(opt).run();
}
結(jié)語
基于JMH可以對很多工具和框架進行測試,比如日志框架性能對比、BeanCopy性能對比 等,更多的example可以參考官方給出的JMH samples(https://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/)
2.?美團一面:兩個有序的數(shù)組,如何高效合并成一個有序數(shù)組?
3.?你用什么軟件做筆記?
最近面試BAT,整理一份面試資料《Java面試BATJ通關手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點“在看”,關注公眾號并回復?Java?領取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)

