關注“Java藝術”一起來充電吧!
JMH
即Java Microbenchmark Harness
,是Java
用來做基準測試的一個工具,該工具由OpenJDK
提供並維護,測試結果可信度高。
基準測試Benchmark
是測量、評估軟件性能指標的一種測試,對某個特定目標場景的某項性能指標進行定量的和可對比的測試。
項目中添加依賴
創建一個基準測試項目,在項目中引入JMH
的jar
包,目前JMH
的最新版本爲1.23
。以maven
爲例,依賴配置如下。
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
</dependency>
</dependencies>
另外,我們也可以直接用在需要進行基準測試的項目中,以單元測試方式使用。
註解方式使用
在運行時,註解配置被用於解析生成BenchmarkListEntry
配置類實例。
一個方法對應一個@Benchmark
註解,一個@Benchmark
註解對應一個基準測試方法。
註釋在類上的註解,或者註釋在類的字段上的註解,則是類中所有基準測試方法共用的配置。
1
@Benchmark
聲明一個public
方法爲基準測試方法。
public class JsonBenchmark {
@Benchmark
@Test // 因爲這是一個單元測試類,所以多了一個@Test註解,可以忽略
public void testGson() {
new GsonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
2
@BenchmarkMode
通過JMH
我們可以輕鬆的測試出某個接口的吞吐量、平均執行時間等指標的數據。
假設我想測試testGson
方法的平均耗時,那麼可以使用@BenchmarkMode
註解指定測試維度爲Mode.AverageTime
。
public class JsonBenchmark {
@BenchmarkMode(Mode.AverageTime) // 指定mode爲Mode.AverageTime
@Benchmark
@Test // 因爲這是一個單元測試類,所以多了一個@Test註解,可以忽略
public void testGson() {
new GsonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
3
@Measurement
假設我想執行testGson
方法五次,取五次執行的耗時平均值,那麼可以使用@Measurement
註解。
public class JsonBenchmark {
@BenchmarkMode(Mode.AverageTime) // 指定mode爲Mode.AverageTime
@Benchmark
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Test // 因爲這是一個單元測試類,所以多了一個@Test註解,可以忽略
public void testGson() {
new GsonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
@Measurement
註解有三個配置項:
iterations
:迭代次數,即執行多少次該方法;time與timeUnit
:執行一次等待的時間,timeUnit
指定時間單位,本例中:執行完一次等待一秒鐘再執行下一次。
4
@Warmup
爲了數據準確,我們可能需要讓testGson
方法做下熱身運動。如在方法中創建GsonParser
對象,預熱可以避免首次創建GsonParser
時因多了類加載的耗時而導致測試結果不準備的情況。jvm
使用JIT
即時編譯器,一定的預熱次數可讓JIT
對testGson
方法的調用鏈路完成編譯,去掉解釋執行對測試結果的影響。
@Warmup
註解用於配置預熱參數。
public class JsonBenchmark {
@BenchmarkMode(Mode.AverageTime) // 指定mode爲Mode.AverageTime
@Benchmark
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Test // 因爲這是一個單元測試類,所以多了一個@Test註解,可以忽略
public void testGson() {
new GsonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
@Warmup
註解有三個配置項:
iterations
:迭代次數,預熱執行多少次該方法;time與timeUnit
:執行一次等待的時間,timeUnit
指定時間單位,本例中:執行完一次等待一秒鐘再執行下一次。
@Warmup
與@Measurement
註解的配置項是一樣的。方法總的迭代執行次數等於@Warmup
與@Measurement
註解的iterations
的總和,前@Warmup
指定的iterations
次數不參與統計。
5
@OutputTimeUnit
OutputTimeUnit
註解用於指定輸出的方法執行耗時的單位。如果方法執行耗時爲秒級別,爲了便於 觀察結果,我們可以使用@OutputTimeUnit
指定輸出的耗時時間單位爲秒;如果方法執行耗時爲毫秒級別,爲了便於觀察結果,我們可以使用@OutputTimeUnit
指定輸出的耗時時間單位爲毫秒,否則使用默認的秒做單位,會輸出10
的負幾次方這樣的數字,不太直觀。
public class JsonBenchmark {
@BenchmarkMode(Mode.AverageTime) // 指定mode爲Mode.AverageTime
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 指定輸出的耗時時長的單位
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Test // 因爲這是一個單元測試類,所以多了一個@Test註解,可以忽略
public void testGson() {
new GsonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
6
@Fork
@Fork
用於指定fork
出多少個子進程來執行同一基準測試方法。假設我們不需要多個進程,那麼 可以使用@Fork
指定爲進程數爲1。
public class JsonBenchmark {
@BenchmarkMode(Mode.AverageTime) // 指定mode爲Mode.AverageTime
@Benchmark
@Fork(1)
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 指定輸出的耗時時長的單位
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Test // 因爲這是一個單元測試類,所以多了一個@Test註解,可以忽略
public void testGson() {
new GsonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
7
@Threads
@Threads
註解用於指定使用多少個線程來執行基準測試方法,如果使用@Threads
指定線程數爲2
,且使用@Fork
指定進程數爲2,那麼兩個進程都會分別創建兩個線程來執行@Measurement
指定的iterations
次基準測試方法。一個進程下的兩個線程執行的方法次數總和等於@Measurement
配置的iterations
。
public class JsonBenchmark {
@BenchmarkMode(Mode.AverageTime) // 指定mode爲Mode.AverageTime
@Benchmark
@Fork(1)
@Threads(2)
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 指定輸出的耗時時長的單位
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Test // 因爲這是一個單元測試類,所以多了一個@Test註解,可以忽略
public void testGson() {
new GsonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
8
公共註解
假設我們需要在JsonBenchmark
類中創建兩個基準測試方法,一個是testGson
,另一個是testJackson
,用於對比Gson
與Jackson
這兩個框架解析json
的性能。那麼我們可以將除@Benchmark
註解外的其它註解都聲明到類上,讓兩個基準測試方法都使用同樣的配置。
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Threads(2)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class JsonBenchmark {
@Benchmark
@Test
public void testGson() {
new GsonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
@Benchmark
@Test
public void testJackson() {
new JacksonParser().fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
如果不想每執行一次方法都創建一個GsonParser
或JacksonParser
實例,我們可以將GsonParser
與JacksonParser
對象聲明爲JsonBenchmark
的字段。(GsonParser
與JacksonParser
是筆者封裝的工具類,配合設計模式使用,爲項目提供隨時切換解析框架的功能)。
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Threads(2)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class JsonBenchmark {
private GsonParser gsonParser = new GsonParser();
private JacksonParser jacksonParser = new JacksonParser();
@Benchmark
@Test
public void testGson() {
gsonParser.fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
@Benchmark
@Test
public void testJackson() {
jacksonParser.fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
還需要使用@State
註解指定字段的共享域。在本例中,我們使用@Threads
註解聲明創建兩個線程來執行基準測試方法,假設我們配置@State(Scope.Thread)
,那麼在不同線程中,gsonParser
、jacksonParser
這兩個字段都是不同的實例。
以testGson
方法爲例,我們可以認爲JMH
會爲每個線程克隆出一個gsonParser
對象。如果在testGson
方法中打印gsonParser
對象的hashCode
,你會發現,相同線程打印的結果相同,不同線程打印的結果不同。例如:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Threads(2)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class JsonBenchmark {
private GsonParser gsonParser = new GsonParser();
private JacksonParser jacksonParser = new JacksonParser();
@Benchmark
@Test
public void testGson() {
System.out.println("current Thread:" + Thread.currentThread().getName() + "==>" + gsonParser.hashCode());
gsonParser.fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
@Benchmark
@Test
public void testJackson() {
jacksonParser.fromJson("{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}", JsonTestModel.class);
}
}
執行testGson
方法輸出的結果如下:
current Thread:com.msyc.common.JsonBenchmark.testGson-jmh-worker-1==>2063684770
current Thread:com.msyc.common.JsonBenchmark.testGson-jmh-worker-2==>1629232880
current Thread:com.msyc.common.JsonBenchmark.testGson-jmh-worker-1==>2063684770
current Thread:com.msyc.common.JsonBenchmark.testGson-jmh-worker-2==>1629232880
current Thread:com.msyc.common.JsonBenchmark.testGson-jmh-worker-1==>2063684770
current Thread:com.msyc.common.JsonBenchmark.testGson-jmh-worker-2==>1629232880
current Thread:com.msyc.common.JsonBenchmark.testGson-jmh-worker-1==>2063684770
current Thread:com.msyc.common.JsonBenchmark.testGson-jmh-worker-2==>1629232880
......
9
@Param
使用@Param
註解可指定基準方法執行參數,@Param
註解只能指定String
類型的值,可以是一個數組,參數值將在運行期間按給定順序遍歷。假設@Param
註解指定了多個參數值,那麼JMH
會爲每個參數值執行一次基準測試。
例如,我們想測試不同複雜度的json
字符串使用Gson
框架與使用Jackson
框架解析的性能對比,代碼如下。
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Threads(2)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
public class JsonBenchmark {
private GsonParser gsonParser = new GsonParser();
private JacksonParser jacksonParser = new JacksonParser();
// 指定參數有三個值
@Param(value =
{"{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 13:00:00\",\"flag\":true,\"threads\":5,\"shardingIndex\":0}",
"{\"startDate\":\"2020-04-01 16:00:00\",\"endDate\":\"2020-05-20 14:00:00\"}",
"{\"flag\":true,\"threads\":5,\"shardingIndex\":0}"})
private String jsonStr;
@Benchmark
@Test
public void testGson() {
gsonParser.fromJson(jsonStr, JsonTestModel.class);
}
@Benchmark
@Test
public void testJackson() {
jacksonParser.fromJson(jsonStr, JsonTestModel.class);
}
}
測試結果如下:
Benchmark (jsonStr) Mode Cnt Score Error Units
JsonBenchmark.testGson {"startDate":"2020-04-01 16:00:00","endDate":"2020-05-20 13:00:00","flag":true,"threads":5,"shardingIndex":0} avgt 5 12180.763 ± 2481.973 ns/op
JsonBenchmark.testGson {"startDate":"2020-04-01 16:00:00","endDate":"2020-05-20 14:00:00"} avgt 5 8154.709 ± 3393.881 ns/op
JsonBenchmark.testGson {"flag":true,"threads":5,"shardingIndex":0} avgt 5 9994.171 ± 5737.958 ns/op
JsonBenchmark.testJackson {"startDate":"2020-04-01 16:00:00","endDate":"2020-05-20 13:00:00","flag":true,"threads":5,"shardingIndex":0} avgt 5 15663.060 ± 9042.672 ns/op
JsonBenchmark.testJackson {"startDate":"2020-04-01 16:00:00","endDate":"2020-05-20 14:00:00"} avgt 5 13776.828 ± 11006.412 ns/op
JsonBenchmark.testJackson {"flag":true,"threads":5,"shardingIndex":0} avgt 5 9824.283 ± 311.555 ns/op
非註解使用
使用註解與不使用註解其實都是一樣,只不過使用註解更加方便。在運行時,註解配置被用於解析生成BenchmarkListEntry
配置類實例,而在代碼中使用Options
配置也是被解析成一個個BenchmarkListEntry
配置類實例(每個方法對應一個)。
非註解方式我們可以使用OptionsBuilder
構造一個Options
,例如,非註解方式實現上面的例子。
public class BenchmarkTest{
@Test
public void test() throws RunnerException {
Options options = new OptionsBuilder()
.include(JsonBenchmark.class.getSimpleName())
.exclude("testJackson")
.forks(1)
.threads(2)
.timeUnit(TimeUnit.NANOSECONDS)
.warmupIterations(5)
.warmupTime(TimeValue.seconds(1))
.measurementIterations(5)
.measurementTime(TimeValue.seconds(1))
.mode(Mode.AverageTime)
.build();
new Runner(options).run();
}
}
include
:導入一個基準測試類。調用方法傳遞的是類的簡單名稱,不含包名。exclude
:排除哪些方法。默認JMH
會爲include
導入的類的每個public
方法都生成一個BenchmarkListEntry
配置類實例,也就是把每個public
方法都當成是基準測試方法,這時我們就可以使用exclude
排除不需要參與基準測試的方法。例如本例中使用exclude
排除了testJackson
方法。
打JAR包放服務器上執行
對於大型的測試,需要測試時間比較久、線程比較多的情況,我們可以將寫好的基準測試項目打包成jar
包丟到linux
服務器上執行。對於吞吐量基準測試,建議放到服務器上執行,其結果會更準確一些,硬件、系統貼近線上環境、也不受本機開啓的應用數、硬件配置等因素影響。
java -jar my-benchmarks.jar
在IDEA中執行
對於一般的方法執行耗時測試,我們不需要把測試放到服務器上執行,例如測試對比幾個json
解析框架的性能。
在idea
中,我們可以編寫一個單元測試方法,在單元測試方法中創建一個org.openjdk.jmh.runner.Runner
,調用Runner
的run
方法執行基準測試。但JMH
不會去掃描包,不會執行每個基準測試方法,這需要我們通過配置項來告知JMH
需要執行哪些基準測試方法。
public class BenchmarkTest{
@Test
public void test() throws RunnerException {
Options options = null; // 創建Options
new Runner(options).run();
}
}
完整例子如下:
public class BenchmarkTest{
@Test
public void test() throws RunnerException {
Options options = new OptionsBuilder()
.include(JsonBenchmark.class.getSimpleName())
// .output("/tmp/json_benchmark.log")
.build();
new Runner(options).run();
}
}
Options
在前面已經介紹過了,由於本例中JsonBenchmark
這個類已經使用了註解,因此Options
只需要配置需要執行基準測試的類。如果需要執行多個基準測試類,include
方法可以多次調用。
如果需要將測試結果輸出到文件,可調用output
方法配置文件路徑,不配置則輸出到控制檯。
在IDEA中使用插件JMH Plugin執行
插件源碼地址:https://github.com/artyushov/idea-jmh-plugin
。
安裝:在IDEA
中搜索JMH Plugin
,安裝後重啓即可使用。
1、只執行單個
Benchmark
方法
在方法名稱所在行,IDEA
會有一個▶️
執行符號,右鍵點擊運行即可。如果寫的是單元測試方法, IDEA
會提示你選擇執行單元測試還是基準測試。
2、執行一個類中的所有
Benchmark
方法
在類名所在行,IDEA
會有一個▶️
執行符號,右鍵點擊運行,該類下的所有被@Benchmark
註解註釋的方法都會執行。如果寫的是單元測試方法,IDEA
會提示你選擇執行單元測試還是基準測試。
官方提供的JMH使用例子
官方的
demo
:
http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
翻譯的
demo
:
https://github.com/Childe-Chen/goodGoodStudy/tree/master/src/main/java/com/cxd/benchmark
公衆號:Java藝術
掃碼關注最新動態