Java JMH進(jìn)行基準(zhǔn)測(cè)試的使用小結(jié)
在 Java 的依賴(lài)庫(kù)中,有個(gè)大名鼎鼎的 JMH(Java Microbenchmark Harness),是由 Java虛擬機(jī)團(tuán)隊(duì)開(kāi)發(fā)的 Java 基準(zhǔn)測(cè)試工具。
在 JMH 中,正如 單元測(cè)試框架 JUnit 一樣,我們也可以通過(guò)大量的注解來(lái)進(jìn)行一定的配置,一個(gè)典型的 JMH 程序執(zhí)行如下圖所示[2]:

也即,通過(guò)開(kāi)啟多個(gè)進(jìn)程,多個(gè)線(xiàn)程,先執(zhí)行預(yù)熱,然后執(zhí)行迭代,最后匯總所有的測(cè)試數(shù)據(jù)進(jìn)行分析,這就是 JMH 的執(zhí)行流程,聽(tīng)起來(lái)是不是不難理解。
1.示例
學(xué)習(xí)新技能通常先通過(guò)一個(gè) case 來(lái)幫準(zhǔn)我們?cè)趺从茫惺裁唇Y(jié)果,這里我們通過(guò)改寫(xiě)官方的一個(gè) sample 來(lái)看看。
package org.example;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
public class HelloWorldBenchmark {
private static int num = 0;
@Benchmark
public void helloWorld() {
++num;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(HelloWorldBenchmark.class.getSimpleName())
.forks(1)
.result("helloWorld.json")
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}
}示例很簡(jiǎn)單,就是簡(jiǎn)單對(duì) static 變量做自增,最后將結(jié)果輸出到 json 文件中,下面是運(yùn)行結(jié)果:
# JMH version: 1.23 # VM version: JDK 21, Java HotSpot(TM) 64-Bit Server VM, 21+35-LTS-2513 # VM invoker: C:\Program Files\Java\jdk-21\bin\java.exe # VM options: -javaagent:D:\chromedownload\ideaIC-2023.2.3.win\lib\idea_rt.jar=55507:D:\chromedownload\ideaIC-2023.2.3.win\bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 # Warmup: 1 iterations, 1 s each # Measurement: 2 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: org.example.HelloWorldBenchmark.helloWorld # Run progress: 0.00% complete, ETA 00:00:03 # Fork: 1 of 1 # Warmup Iteration 1: 1659899825.872 ops/s Iteration 1: 1646745186.884 ops/s Iteration 2: 1681125023.980 ops/s Result "org.example.HelloWorldBenchmark.helloWorld": 1663935105.432 ops/s # Run complete. Total time: 00:00:03 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 HelloWorldBenchmark.helloWorld thrpt 2 1663935105.432 ops/s Benchmark result is saved to helloWorld.json
通過(guò)簡(jiǎn)單的設(shè)置,我們?cè)诨鶞?zhǔn)測(cè)試中可以看到多次測(cè)試的每秒吞吐量,最后結(jié)果輸出到 helloWorldjson 文件:
[
{
"jmhVersion" : "1.23",
"benchmark" : "org.example.HelloWorldBenchmark.helloWorld",
"mode" : "thrpt",
"threads" : 1,
"forks" : 1,
"jvm" : "C:\Program Files\Java\jdk-21\bin\java.exe",
"jvmArgs" : [
"-javaagent:D:\chromedownload\ideaIC-2023.2.3.win\lib\idea_rt.jar=55507:D:\chromedownload\ideaIC-2023.2.3.win\bin",
"-Dfile.encoding=UTF-8",
"-Dsun.stdout.encoding=UTF-8",
"-Dsun.stderr.encoding=UTF-8"
],
"jdkVersion" : "21",
"vmName" : "Java HotSpot(TM) 64-Bit Server VM",
"vmVersion" : "21+35-LTS-2513",
"warmupIterations" : 1,
"warmupTime" : "1 s",
"warmupBatchSize" : 1,
"measurementIterations" : 2,
"measurementTime" : "1 s",
"measurementBatchSize" : 1,
"primaryMetric" : {
"score" : 1.6639351054317546E9,
"scoreError" : "NaN",
"scoreConfidence" : [
"NaN",
"NaN"
],
"scorePercentiles" : {
"0.0" : 1.6467451868835843E9,
"50.0" : 1.6639351054317546E9,
"90.0" : 1.6811250239799252E9,
"95.0" : 1.6811250239799252E9,
"99.0" : 1.6811250239799252E9,
"99.9" : 1.6811250239799252E9,
"99.99" : 1.6811250239799252E9,
"99.999" : 1.6811250239799252E9,
"99.9999" : 1.6811250239799252E9,
"100.0" : 1.6811250239799252E9
},
"scoreUnit" : "ops/s",
"rawData" : [
[
1.6467451868835843E9,
1.6811250239799252E9
]
]
},
"secondaryMetrics" : {
}
}
]看完怎么用,接下來(lái)看看在項(xiàng)目中注意的點(diǎn)和值得注意的參數(shù)注解。
2.JMH的使用
引入依賴(lài)
由于這不是標(biāo)準(zhǔn)庫(kù)有的依賴(lài),所以這里我們依然用 Maven 管理依賴(lài),在我們構(gòu)建的 Maven 項(xiàng)目中的 pom.xml 添加下列依賴(lài):
<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>接下來(lái)看看代碼應(yīng)用。
代碼示例基于參考編寫(xiě):
package org.example;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@Threads(2)
public class MyBenchmarkTest {
@Benchmark
public long shift() {
long t = 455565655225562L;
long a = 0;
for (int i = 0; i < 1000; i++) {
a = t >> 30;
}
return a;
}
@Benchmark
public long div() {
long t = Long.MAX_VALUE;
long a = 0;
for (int i = 0; i < 1000; i++) {
a = t / 1024 / 1024 / 1024;
}
return a;
}
public static void main(String[] args) throws RunnerException {
Options opts = new OptionsBuilder()
.include(MyBenchmarkTest.class.getSimpleName())
.result("MyBenchmarkTest.json")
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opts).run();
}
}在示例中,其實(shí)我們的目的就是測(cè)試 移位和整除 兩個(gè)方法的性能,看看每秒的吞吐量如何,最后將結(jié)果匯總在 MyBenchmarkTest.json 文件中,當(dāng)然運(yùn)行測(cè)試,我們也可以在控制臺(tái)得到相應(yīng)輸出。
注解
在上面的 demo 中,我們?cè)陬?lèi)上加了很多注解,注解的作用又是啥呢?
@BenchmarkMode
該注解用來(lái)指定基準(zhǔn)測(cè)試類(lèi)型,對(duì)應(yīng) Mode 選項(xiàng),修飾類(lèi)和方法,這里我們修飾類(lèi),注解的 value 是 Mode[] 類(lèi)型,我們這里填入的是 Throughput ,表示整體吞吐量,即單位時(shí)間內(nèi)的調(diào)用量,查看 Mode 源碼就可以發(fā)現(xiàn),其實(shí)總的類(lèi)型有以下:
Throughput: 略
AverageTime: 平均耗時(shí),指的是每次執(zhí)行的平均時(shí)間。如果這個(gè)值很小不好辨認(rèn),可以把統(tǒng)計(jì)的單位時(shí)間調(diào)小一點(diǎn)。
SampleTime: 隨機(jī)取樣。
SingleShotTime: 如果你想要測(cè)試僅僅一次的性能,比如第一次初始化花了多長(zhǎng)時(shí)間,就可以使用這個(gè)參數(shù),其實(shí)和傳統(tǒng)的main方法沒(méi)有什么區(qū)別。
All: 所有的指標(biāo),都算一遍。
從 控制臺(tái)的結(jié)果可以看看相關(guān)輸出:
Benchmark Mode Cnt Score Error Units
MyBenchmarkTest.div thrpt 5 500758.115 ± 3350.796 ops/ms
MyBenchmarkTest.shift thrpt 5 500045.811 ± 1609.779 ops/ms
如果填入 Mode.All 看看結(jié)果輸出:
Benchmark Mode Cnt Score Error Units
MyBenchmarkTest.div thrpt 5 500554.176 ± 8015.731 ops/ms
MyBenchmarkTest.shift thrpt 5 499731.423 ± 4635.160 ops/ms
MyBenchmarkTest.div avgt 5 ≈ 10?? ms/op
MyBenchmarkTest.shift avgt 5 ≈ 10?? ms/op
MyBenchmarkTest.div sample 316909 ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.00 sample ≈ 0 ms/op
MyBenchmarkTest.div:div·p0.50 sample ≈ 0 ms/op
MyBenchmarkTest.div:div·p0.90 sample ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.95 sample ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.99 sample ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.999 sample ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.9999 sample 0.002 ms/op
MyBenchmarkTest.div:div·p1.00 sample 0.025 ms/op
MyBenchmarkTest.shift sample 315964 ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.00 sample ≈ 0 ms/op
MyBenchmarkTest.shift:shift·p0.50 sample ≈ 0 ms/op
MyBenchmarkTest.shift:shift·p0.90 sample ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.95 sample ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.99 sample ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.999 sample ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.9999 sample 0.001 ms/op
MyBenchmarkTest.shift:shift·p1.00 sample 0.024 ms/op
MyBenchmarkTest.div ss 5 0.052 ± 0.091 ms/op
MyBenchmarkTest.shift ss 5 0.015 ± 0.023 ms/op
此時(shí)可以看到十分詳盡的輸出,每秒的吞吐量,每個(gè)操作的耗費(fèi)時(shí)間等,因?yàn)楸纠?jiǎn)單,時(shí)間耗費(fèi)建議填入 ns 等單位。
@BenchmarkMode 表示單位時(shí)間的操作數(shù)或者吞吐量,或者每個(gè)操作耗費(fèi)的時(shí)間等,注意我們都沒(méi)有限定時(shí)間單位,所以通常這個(gè)注解也會(huì)和 @OutputTimeUnit 結(jié)合使用。
@OutputTimeUnit
基準(zhǔn)測(cè)試結(jié)果的時(shí)間類(lèi)型。一般選擇秒、毫秒、微秒,這里填入的是 TimeUnit 這個(gè)枚舉類(lèi)型,涉及單位很多從納秒到天都有,按需選擇,最終輸出易讀的結(jié)果。
@State
@State 指定了在類(lèi)中變量的作用范圍。它有三個(gè)取值。
@State 用于聲明某個(gè)類(lèi)是一個(gè)“狀態(tài)”,可以用Scope 參數(shù)用來(lái)表示該狀態(tài)的共享范圍。這個(gè)注解必須加在類(lèi)上,否則提示無(wú)法運(yùn)行。
Scope有如下3種值:
- Benchmark:表示變量的作用范圍是某個(gè)基準(zhǔn)測(cè)試類(lèi)。
- Thread:每個(gè)線(xiàn)程一份副本,如果配置了Threads注解,則每個(gè)Thread都擁有一份變量,它們互不影響。
- Group:聯(lián)系上面的@Group注解,在同一個(gè)Group里,將會(huì)共享同一個(gè)變量實(shí)例。
本例中,相關(guān)變量的作用范圍是 Thread。
@Warmup
預(yù)熱,可以加在類(lèi)上或者方法上,預(yù)熱只是測(cè)試數(shù)據(jù),是不作為測(cè)量結(jié)果的。
該注解一共有4個(gè)參數(shù):
- iterations 預(yù)熱階段的迭代數(shù)
- time 每次預(yù)熱時(shí)間
- timeUnit 時(shí)間單位,通常秒
- batchSize 批處理大小,指定每次操作調(diào)用幾次方法
本例中,我們加在類(lèi)上,讓它迭代3次,每次1秒,時(shí)間單位秒。
@Measurement
和預(yù)熱類(lèi)似,這里的注解是會(huì)影響測(cè)試結(jié)果的,它的參數(shù)和 Warmup 一樣,這里不多介紹。
本例中我們?cè)诘性O(shè)置的是5次,每次1秒。
通常 @Warmup 和 @Measurement 兩個(gè)參數(shù)會(huì)一起使用。
@Fork
表示開(kāi)啟幾個(gè)進(jìn)程測(cè)試,通常我們?cè)O(shè)為1,如果數(shù)值大于1,則啟用新的進(jìn)程測(cè)試,如果設(shè)置為0,程序依然進(jìn)行,但是在用戶(hù)的 JVM 進(jìn)程上運(yùn)行。
追蹤一下JMH的源碼,發(fā)現(xiàn)每個(gè)fork進(jìn)程是單獨(dú)運(yùn)行在Proccess進(jìn)程里的,這樣就可以做完全的環(huán)境隔離,避免交叉影響。它的輸入輸出流,通過(guò)Socket連接的模式,發(fā)送到我們的執(zhí)行終端。

如果需要更多的設(shè)置,可以看看 Fork.class 源碼,上面還有 jvm 參數(shù)設(shè)置。
@Threads
上面的注解注重開(kāi)啟幾個(gè)進(jìn)程,這里就是開(kāi)啟幾個(gè)線(xiàn)程,只有一個(gè)參數(shù) value,指定注解的value,將會(huì)開(kāi)啟并行測(cè)試,如果設(shè)置的 value 過(guò)大,如 Threads.Max,則使用處理機(jī)的相同線(xiàn)程數(shù)。
@Benchmark
加在測(cè)試方法上,表示該方法是需要進(jìn)行基準(zhǔn)測(cè)試的,類(lèi)似 JUnit5 中的 @Test 注解需要單元測(cè)試的方法一樣。
@Setup
注解的作用就是我們需要在測(cè)試之前進(jìn)行一些準(zhǔn)備工作,比如對(duì)一些數(shù)據(jù)的初始化之類(lèi)的,這個(gè)也和Junit的@Before
@Teardown
在測(cè)試之后進(jìn)行一些結(jié)束工作,主要用于資源回收
開(kāi)啟測(cè)試
上述的學(xué)習(xí)中主要是相關(guān)注解,這里看看具體我們?cè)趺从谩?/p>
public static void main(String[] args) throws RunnerException {
Options opts = new OptionsBuilder()
// 表示包含的測(cè)試類(lèi)
.include(MyBenchmarkTest.class.getSimpleName())
// 最后結(jié)果輸出文件的命名
.result("MyBenchmarkTest.json")
// 結(jié)果輸出什么格式,可以是json, csv, text等
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opts).run(); // 運(yùn)行
}3.JMH可視化
作為程序開(kāi)發(fā)人員,看懂測(cè)試結(jié)果沒(méi)難度,測(cè)試結(jié)果文本能可視化更好。
好在我們拿到了JMH 結(jié)果后,根據(jù)文件格式,我們可以二次加工,就可以圖表化展示[2]。
JMH 支持的幾種輸出格式:
- TEXT 導(dǎo)出文本文件。
- CSV 導(dǎo)出csv格式文件。
- SCSV 導(dǎo)出scsv等格式的文件。
- JSON 導(dǎo)出成json文件。
- LATEX 導(dǎo)出到latex,一種基于ΤΕΧ的排版系統(tǒng)。
到此這篇關(guān)于Java JMH進(jìn)行基準(zhǔn)測(cè)試的使用小結(jié)的文章就介紹到這了,更多相關(guān)Java JMH基準(zhǔn)測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java集合ArrayDeque類(lèi)實(shí)例分析
這篇文章主要介紹了Java集合ArrayDeque類(lèi)實(shí)例分析的相關(guān)資料,需要的朋友可以參考下2017-04-04
Java編程線(xiàn)程間通信與信號(hào)量代碼示例
這篇文章主要介紹了Java編程線(xiàn)程間通信與信號(hào)量代碼示例,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12
Java循環(huán)創(chuàng)建對(duì)象內(nèi)存溢出的解決方法
在Java中,如果在循環(huán)中不當(dāng)?shù)貏?chuàng)建大量對(duì)象而不及時(shí)釋放內(nèi)存,很容易導(dǎo)致內(nèi)存溢出(OutOfMemoryError),所以本文給大家介紹了Java循環(huán)創(chuàng)建對(duì)象內(nèi)存溢出的解決方法,需要的朋友可以參考下2025-01-01
feign調(diào)用中文參數(shù)被encode編譯的問(wèn)題
這篇文章主要介紹了feign調(diào)用中文參數(shù)被encode編譯的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
SpringBoot?web靜態(tài)資源映射實(shí)現(xiàn)步驟詳解
在springBoot中的靜態(tài)資源的映射是通過(guò)SpringMVC中的resourceHttpRequestHandler來(lái)進(jìn)行實(shí)現(xiàn)的。在該請(qǐng)求映射器中默認(rèn)規(guī)定了,SpringBoot會(huì)將classPath或者ServletContext下的/static?(/public、/resources?或?/META-INF/resources)目錄中,存放靜態(tài)資源2022-09-09
Spring Transaction事務(wù)實(shí)現(xiàn)流程源碼解析
此文就Spring 事務(wù)實(shí)現(xiàn)流程進(jìn)行源碼解析,我們可以借此對(duì)Spring框架更多一層理解,下面以xml形式創(chuàng)建一個(gè)事務(wù)進(jìn)行分析2022-09-09
JVM線(xiàn)上調(diào)優(yōu)參數(shù)配置實(shí)踐完全指南
JVM調(diào)優(yōu)的基本思路涉及多個(gè)方面,旨在提升Java應(yīng)用的性能、穩(wěn)定性和響應(yīng)速度,下面這篇文章主要介紹了JVM線(xiàn)上調(diào)優(yōu)參數(shù)配置的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-08-08
Spring Boot 通過(guò)注解實(shí)現(xiàn)數(shù)據(jù)校驗(yàn)的方法
這篇文章主要介紹了Spring Boot 通過(guò)注解實(shí)現(xiàn)數(shù)據(jù)校驗(yàn)的方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09

