Table of Contents
一. 簡介
JMH,全稱 Java Microbenchmark Harness (微基準測試框架),是專門用於Java代碼微基準測試的一套測試工具API,是由 OpenJDK/Oracle 官方發佈的工具。其精度可以達到毫秒級.可以執行一個函數需要多少時間,或者一個算法有多種不同實現等情況下,選擇一個性能最好的那個.
Java的基準測試需要注意的幾個點:
- 測試前需要預熱。
- 防止無用代碼進入測試方法中。
- 併發測試。
- 測試結果呈現。
預熱? 爲什麼要預熱? ???????????
因爲 JVM 的 JIT 機制的存在,如果某個函數被調用多次之後,JVM 會嘗試將其編譯成爲機器碼從而提高執行速度。爲了讓 benchmark 的結果更加接近真實情況就需要進行預熱。
JMH的使用場景:
- 定量分析某個熱點函數的優化效果
- 想定量地知道某個函數需要執行多長時間,以及執行時間和輸入變量的相關性
- 對比一個函數的多種實現方式
二. 安裝 [ idea plug ]
2.1 直接在Plugins的面板搜索 JMH 安裝JMH plugin .
2.2 配置idea , 即可.
三. 註解
@Benchmark
@Benchmark標籤是用來標記測試方法的,只有被這個註解標記的話,該方法纔會參與基準測試,但是有一個基本的原則就是被@Benchmark標記的方法必須是public的。
@Warmup
@Warmup用來配置預熱的內容,可用於類或者方法上,越靠近執行方法的地方越準確。一般配置warmup的參數有這些:
-
iterations:預熱的次數。
-
time:每次預熱的時間。
-
timeUnit:時間單位,默認是s。
-
batchSize:批處理大小,每次操作調用幾次方法。(後面用到)
-
@Measurement
用來控制實際執行的內容,配置的選項本warmup一樣。
@BenchmarkMode
@BenchmarkMode主要是表示測量的維度,有以下這些維度可供選擇:
Mode.Throughput 吞吐量維度
Mode.AverageTime 平均時間
Mode.SampleTime 抽樣檢測
Mode.SingleShotTime 檢測一次調用
Mode.All 運用所有的檢測模式 在方法級別指定@BenchmarkMode的時候可以一定指定多個緯度,例如: @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}),代表同時在多個緯度對目標方法進行測量。
@OutputTimeUnit
@OutputTimeUnit代表測量的單位,比如秒級別,毫秒級別,微妙級別等等。一般都使用微妙和毫秒級別的稍微多一點。該註解可以用在方法級別和類級別,當用在類級別的時候會被更加精確的方法級別的註解覆蓋,原則就是離目標更近的註解更容易生效。
@State
在很多時候我們需要維護一些狀態內容,比如在多線程的時候我們會維護一個共享的狀態,這個狀態值可能會在每隔線程中都一樣,也有可能是每個線程都有自己的狀態,JMH爲我們提供了狀態的支持。該註解只能用來標註在類上,因爲類作爲一個屬性的載體。 @State的狀態值主要有以下幾種:
- Scope.Benchmark 該狀態的意思是會在所有的Benchmark的工作線程中共享變量內容。
- Scope.Group 同一個Group的線程可以享有同樣的變量
- Scope.Thread 每隔線程都享有一份變量的副本,線程之間對於變量的修改不會相互影響。
下面看兩個常見的@State的寫法:
1.直接在內部類中使用@State作爲“PropertyHolder”
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++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_03_States.class.getSimpleName())
.threads(4)
.forks(1)
.build();
new Runner(opt).run();
}
}
2.在Main類中直接使用@State作爲註解,是Main類直接成爲“PropertyHolder”
@State(Scope.Thread)
public class JMHSample_04_DefaultState {
double x = Math.PI;
@Benchmark
public void measure() {
x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_04_DefaultState.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
我們試想以下@State的含義,它主要是方便框架來控制變量的過程邏輯,通過@State標示的類都被用作屬性的容器,然後框架可以通過自己的控制來配置不同級別的隔離情況。被@Benchmark標註的方法可以有參數,但是參數必須是被@State註解的,就是爲了要控制參數的隔離。
但是有些情況下我們需要對參數進行一些初始化或者釋放的操作,就像Spring提供的一些init和destory方法一樣,JHM也提供有這樣的鉤子:
-
@Setup 必須標示在@State註解的類內部,表示初始化操作
-
@TearDown 必須表示在@State註解的類內部,表示銷燬操作
-
初始化和銷燬的動作都只會執行一次。
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個實例
public class JMHSample_05_StateFixtures {
double x;
@Setup
public void prepare() {
System.out.println("This is Setup ");
x = Math.PI;
}
@TearDown
public void check() {
System.out.println("This is TearDown ");
assert x > Math.PI : "Nothing changed?";
}
@Benchmark
public void measureRight() {
x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_05_StateFixtures.class.getSimpleName())
.forks(1)
.jvmArgs("-ea")
.build();
new Runner(opt).run();
}
}
雖然我們可以執行初始化和銷燬的動作,但是總是感覺還缺點啥?對,就是初始化的粒度。因爲基準測試往往會執行多次,那麼能不能保證每次執行方法的時候都初始化一次變量呢? @Setup和@TearDown提供了以下三種緯度的控制:
Level.Trial 只會在個基礎測試的前後執行。包括Warmup和Measurement階段,一共只會執行一次。
Level.Iteration 每次執行記住測試方法的時候都會執行,如果Warmup和Measurement都配置了2次執行的話,那麼@Setup和@TearDown配置的方法的執行次數就4次。
Level.Invocation 每個方法執行的前後執行(一般不推薦這麼用)
@Param
在很多情況下,我們需要測試不同的參數的不同結果,但是測試的了邏輯又都是一樣的,因此如果我們編寫鍍鉻benchmark的話會造成邏輯的冗餘,幸好JMH提供了@Param參數來幫助我們處理這個事情,被@Param註解標示的參數組會一次被benchmark消費到。
@State(Scope.Benchmark)
public class ParamTest {
@Param({"1", "2", "3"})
int testNum;
@Benchmark
public String test() {
return String.valueOf(testNum);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ParamTest.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
@Threads
測試線程的數量,可以配置在方法或者類上,代表執行測試的線程數量。
通常看到這裏我們會比較迷惑Iteration和Invocation區別,我們在配置Warmup的時候默認的時間是的1s,即1s的執行作爲一個Iteration,假設每次方法的執行是100ms的話,那麼1個Iteration就代表10個Invocation。
四.使用樣例
4.1.修改pom.xml配置文件
<dependencies>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
<scope>provided</scope>
</dependency>
</dependencies>
4.2.測試map的循環輸出效率
4.2.1.代碼
package com.map;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次1s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個實例
public class HashMapTest {
static Map<Integer, String> map = new HashMap() {{
// 添加數據
for (int i = 0; i < 10000; i++) {
put(i, "val:" + i);
}
}};
public static void main(String[] args) throws RunnerException {
// 啓動基準測試
Options opt = new OptionsBuilder()
.include(HashMapTest.class.getSimpleName()) // 要導入的測試類
// .output("/a/jmh-map.log") // 輸出測試結果的文件
.build();
new Runner(opt).run(); // 執行測試
}
@Benchmark
public void entrySet() {
// 遍歷
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
Integer k = entry.getKey();
String v = entry.getValue();
}
}
@Benchmark
public void forEachEntrySet() {
// 遍歷
for (Map.Entry<Integer, String> entry : map.entrySet()) {
Integer k = entry.getKey();
String v = entry.getValue();
}
}
@Benchmark
public void keySet() {
// 遍歷
Iterator<Integer> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
Integer k = iterator.next();
String v = map.get(k);
}
}
@Benchmark
public void forEachKeySet() {
// 遍歷
for (Integer key : map.keySet()) {
Integer k = key;
String v = map.get(k);
}
}
@Benchmark
public void lambda() {
// 遍歷
map.forEach((key, value) -> {
Integer k = key;
String v = map.get(k);
});
}
@Benchmark
public void streamApi() {
// 單線程遍歷
map.entrySet().stream().forEach((entry) -> {
Integer k = entry.getKey();
String v = entry.getValue();
});
}
public void parallelStreamApi() {
// 多線程遍歷
map.entrySet().parallelStream().forEach((entry) -> {
Integer k = entry.getKey();
String v = entry.getValue();
});
}
}
4.2.2.結果:
儘量使用 map.entrySet() 進行輸出. 因爲性能方面照普通的循環快將近2倍.
Benchmark Mode Cnt Score Error Units
HashMapTest.entrySet avgt 5 47831.209 ± 14037.126 ns/op
HashMapTest.forEachEntrySet avgt 5 56189.356 ± 16866.246 ns/op
HashMapTest.forEachKeySet avgt 5 75973.584 ± 7738.707 ns/op
HashMapTest.keySet avgt 5 76458.791 ± 11008.914 ns/op
HashMapTest.lambda avgt 5 80734.554 ± 9695.806 ns/op
HashMapTest.streamApi avgt 5 49939.633 ± 11131.095 ns/op
4.2.3.原因.
EntrySet
之所以比 KeySet
的性能高是因爲,KeySet
在循環時使用了 map.get(key)
,而 map.get(key)
相當於又遍歷了一遍 Map 集合去查詢 key
所對應的值。爲什麼要用“又”這個詞?那是因爲在使用迭代器或者 for 循環時,其實已經遍歷了一遍 Map 集合了,因此再使用 map.get(key)
查詢時,相當於遍歷了兩遍。
而 EntrySet
只遍歷了一遍 Map 集合,之後通過代碼“Entry<Integer, String> entry = iterator.next()”把對象的 key
和 value
值都放入到了 Entry
對象中,因此再獲取 key
和 value
值時就無需再遍歷 Map 集合,只需要從 Entry
對象中取值就可以了。
所以,EntrySet
的性能比 KeySet
的性能高出了一倍,因爲 KeySet
相當於循環了兩遍 Map 集合,而 EntrySet
只循環了一遍。
4.3.測試list的循環效果
4.3.1.代碼
package list;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import java.util.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個實例
public class ListTest {
static LinkedList<Integer> linkedList = new LinkedList<Integer>(){
{
// 添加數據
for (int i = 0; i < 10000; i++) {
add(i);
}
}
};
static ArrayList<Integer> arrayList = new ArrayList<Integer>(){
{
// 添加數據
for (int i = 0; i < 10000; i++) {
add(i);
}
}
};
public static void main(String[] args) throws RunnerException {
// 啓動基準測試
Options opt = new OptionsBuilder()
.include(ListTest.class.getSimpleName()) // 要導入的測試類
// .output("/a/jmh-map.log") // 輸出測試結果的文件
.build();
new Runner(opt).run(); // 執行測試
}
@Benchmark
public void linkedListFor() {
// 遍歷
for (int i = 0 ; i < linkedList.size() ; i++) {
int x = linkedList.get(i);
}
}
@Benchmark
public void linkedListForEach() {
// 遍歷
for (Integer i : linkedList ) {
}
}
@Benchmark
public void arrayListFor() {
// 遍歷
for (int i = 0 ; i < arrayList.size() ; i++) {
int x = arrayList.get(i);
}
}
@Benchmark
public void arrayListForEach() {
// 遍歷
for (Integer i : arrayList ) {
}
}
}
4.3.2. 結果
Benchmark Mode Cnt Score Error Units
ListTest.arrayListFor avgt 5 7291.515 ± 1348.462 ns/op
ListTest.arrayListForEach avgt 5 6955.242 ± 1587.509 ns/op
ListTest.linkedListFor avgt 5 41467520.339 ± 8443367.361 ns/op
ListTest.linkedListForEach avgt 5 22096.751 ± 6459.100 ns/op
五.官方網站&示例
官網: https://openjdk.java.net/projects/code-tools/jmh/
git hub idea-jmh-plugin : https://github.com/artyushov/idea-jmh-plugin