文章目錄
- Pre
- 引言
- 1. JMH 簡介
- 2. JMH 執行流程詳解
- 3. 關鍵注解詳解
- 3.1 @Warmup
- 3.2 @Measurement
- 3.3 @BenchmarkMode
- 3.4 @OutputTimeUnit
- 3.5 @Fork
- 3.6 @Threads
- 3.7 @Group 與 @GroupThreads
- 3.8 @State
- 3.9 @Setup 與 @TearDown
- 3.10 @Param
- 3.11 @CompilerControl
- 4. 示例代碼與分析
- 4.1 關鍵點解讀
- 4.2 運行與結果示例
- 5. 結果可視化與二次處理
- 5.1 支持的輸出格式
- 5.2 可視化工具推薦
- 小結
Pre
性能優化 - 理論篇:常見指標及切入點
性能優化 - 理論篇:性能優化的七類技術手段
性能優化 - 理論篇:CPU、內存、I/O診斷手段
性能優化 - 工具篇:常用的性能測試工具
- 引言:手工計時的局限,說明 JMH 的必要性與優勢;
- JMH 簡介:工具背景、引入方式(JDK 12 內置與 Maven 依賴);
- JMH 執行流程:Fork、Threads、Warmup、Measurement 四階段說明;
- 關鍵注解詳解:
4.1 @Warmup:預熱原理與配置要點;
4.2 @Measurement:測量階段的迭代策略;
4.3 @BenchmarkMode:常見模式及輸出含義;
4.4 @OutputTimeUnit:結果單位切換;
4.5 @Fork:進程隔離與 JVM 參數傳遞;
4.6 @Threads:并發線程數控制;
4.7 @Group 與 @GroupThreads(簡單介紹);
4.8 @State:狀態作用域(Benchmark/Thread/Group);
4.9 @Setup 與 @TearDown:初始化與清理時機;
4.10 @Param:不同參數組合測試法;
4.11 @CompilerControl:強制內聯與編譯控制; - 示例代碼分析:對比 shift 與 div 兩種寫法的基準測試;
- 結果可視化:JMH 支持的五種輸出格式,常用 CSV/JSON 導出;以及三款可視化工具(JMH Visualizer、JMH Visual Chart、meta-chart);
- 小結:JMH 在熱點代碼驗證與專項微基準測試中的應用價值;
引言
在以往的項目開發與性能調優中,我們經常會寫類似如下的簡單計時代碼:
long start = System.currentTimeMillis();
// logic
long cost = System.currentTimeMillis() - start;
System.out.println("Logic cost : " + cost);
然而,這種方式往往并不準確:
-
JIT 編譯與方法內聯
- 在 HotSpot JVM 中,編譯器會對“熱點方法”進行 JIT 編譯并嘗試內聯;
- 如果僅執行一次計時,沒有經過充分“預熱”,那就是測的是“解釋執行”甚至是“部分編譯前”的性能。
-
測量誤差與噪聲干擾
- 操作系統調度、垃圾回收、上下文切換、CPU 頻率變動等因素,都可能導致
System.currentTimeMillis()
級別的測量出現幾十毫秒甚至幾百毫秒的抖動; - 對于微基準測試(microbenchmark),通常期望測量精度達到“納秒”級別,需要靠更專業的工具來保證。
- 操作系統調度、垃圾回收、上下文切換、CPU 頻率變動等因素,都可能導致
-
重復測量與結果收斂
- 如果邏輯執行速度非常快,僅靠一次或少量循環,很難區分不同實現間的微小差距;
- 必須反復多次執行、結合預熱階段,才能進入“JIT 穩定態”,得到接近真實的執行時間。
基于上述原因,JMH(Java Microbenchmark Harness) 應運而生。JMH 是 OpenJDK 官方團隊維護的微基準測試框架,內置在 JDK 12 及更高版本中(早期需要通過 Maven 坐標引入)。它能夠以納秒級精度準確地測量 Java 方法的吞吐量與延遲,同時自動完成:
- 多輪預熱(Warmup)以驅動 JIT 編譯;
- 多次迭代測量(Measurement)并收集統計數據;
- 多進程隔離(Fork)確保 JVM 參數與環境一致;
- 并發線程控制(Threads)模擬多線程競爭場景。
1. JMH 簡介
-
起源與背景
- JMH 最初由 OpenJDK 團隊開發,用于給 JVM 本身的性能回歸測試提供微基準支持;
- 隨后廣泛應用于社區,被各大中間件與框架團隊用來驗證算法、數據結構或方法優化效果。
-
引入方式
-
JDK12+ 內置:如果使用 JDK 12 或更高版本,默認包含了
jmh-core
與jmh-generator-annprocess
; -
Maven/Gradle 依賴(適用于 JDK11 及以下版本):
<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><scope>provided</scope></dependency> </dependencies>
-
IDE 集成:將上述依賴導入后,IDE(如 IntelliJ IDEA)會識別
@Benchmark
注解并支持直接運行;
-
-
基本原理
JMH 通過注解與代碼生成器,將被測試的方法包裝成專用的 Runner(子進程),在子進程內執行多輪預熱與測量,并將數據通過 Socket 或共享內存傳回主進程,最后統一匯總、統計并輸出。整個流程大致如下:
- Fork(進程隔離):JMH 啟動指定數量的子進程,每個子進程都運行相同的測試。
- Warmup(預熱):在每個子進程中,對目標方法執行若干輪預熱(可配置輪數和時長),驅動 JIT 編譯與內聯優化。
- Measurement(正式測量):預熱結束后,進入正式測量階段,同樣執行可配置輪數和時長,收集方法執行耗時或吞吐統計。
- Result 收集與輸出:每個子進程將自身測量結果發送給主進程,主進程匯總所有子進程數據并最終輸出(TEXT/CSV/JSON/LATEX/…)。
2. JMH 執行流程詳解
在 JMH 運行日志中,你會看到類似下面的輸出:
# JMH version: 1.23
# VM version: JDK 11.0.5, Java HotSpot(TM) 64-Bit Server VM, 11.0.5+10
# Warmup: 3 iterations, 1 s each
# Warmup Iteration 1: 0.281 ops/ns
# Warmup Iteration 2: 0.376 ops/ns
# Warmup Iteration 3: 0.483 ops/ns
# Measurement: 5 iterations, 1 s each
# Fork: 1 of 1
# Threads: 2 threads
# Benchmark: com.example.BenchmarkTest.shiftIteration 1: 1646.000 ns/op
Iteration 2: 1243.000 ns/op
Iteration 3: 1273.000 ns/op
Iteration 4: 1395.000 ns/op
Iteration 5: 1423.000 ns/opResult "com.example.BenchmarkTest.shift":2.068 ±(99.9%) 0.038 ns/op [Average](min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010CI (99.9%): [2.030, 2.106] (assumes normal distribution)
下面將結合這段日志逐項說明流程與參數含義:
-
Warmup(預熱)
- 例中配置為
3 iterations, 1 s each
,表示在每個 Fork 進程中,先進行 3 輪預熱,每輪持續 1 秒,將預熱階段測得的吞吐(ops/ns)丟棄,不納入最終統計; - 預熱結束后,JMH 會自動等待 JIT 完成優化,使得被測方法進入“穩態”(steady-state)狀態,從而讓 Measurement 階段結果更準確。
- 例中配置為
-
Measurement(測量)
- 配置為
5 iterations, 1 s each
,意味著正式測量階段同樣分 5 輪,每輪持續 1 秒,將每輪測量結果記錄到輸出; - 日志中每行
Iteration X: YYY ns/op
,即表示第 X 輪的平均耗時(以納秒為單位); - JMH 會將所有輪次的測量結果匯總,計算整體的平均值(avg)、標準差(stdev)、95% 或 99.9% 置信區間(CI)等統計指標。
- 配置為
-
Fork(進程隔離)
-
例中為
Fork: 1 of 1
,表示只啟動一個子進程進行測試;如果配置成@Fork(3)
,JMH 會依次啟動 3 個子進程,每個子進程重復上述 Warmup + Measurement 流程,最終在主進程以更大樣本量做全局聚合; -
通過多 Fork 可以減少單個子進程環境的干擾(如 GC、操作系統調度差異)對結果的影響;
-
如果設置
@Fork(0)
,則表示“非 Fork 模式”,會直接在當前 JVM 進程中執行所有輪次。但 JMH 官方強烈不推薦非 Fork 模式在正式測量時使用,會提示警告:# *** WARNING: Non-forked runs may silently omit JVM options, mess up profilers, disable compiler hints, etc. ***
-
Fork
注解支持參數jvmArgsAppend
,可以為每個子進程傳入不同的 JVM 參數,例如:@Fork(value = 3, jvmArgsAppend = {"-Xmx2048m", "-server", "-XX:+AggressiveOpts"})
這樣每個子進程都會以
-Xmx2048m -server -XX:+AggressiveOpts
的參數啟動,更精細地控制測量環境。
-
-
Threads(線程并發)
- 例中日志
# Threads: 2 threads
,對應@Threads(2)
注解,表示每個子進程都會啟動 2 個并行線程同時執行被測方法,以測試并發場景下的吞吐或延遲; - 如果配置
@Threads(Threads.MAX)
,JMH 會自動根據機器上的邏輯 CPU 核心數創建同等數量的線程; - 線程數過多可能導致上下文切換過于頻繁,從而影響測量結果,需要根據測試目標(單線程性能 vs 多線程吞吐)以及硬件條件合理設置。
- 例中日志
3. 關鍵注解詳解
下面一一介紹 JMH 常用注解的語義、配置參數與注意事項。
3.1 @Warmup
@Warmup(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS,batchSize = 1 // 可選
)
-
用途:配置預熱階段的迭代輪數與持續時長,讓被測方法在 Measurement 階段之前盡量被 JIT 編譯、內聯完成。
-
參數說明:
iterations
:預熱輪數,表示要執行多少輪預熱;time
+timeUnit
:每輪預熱的持續時間,例如1, TimeUnit.SECONDS
表示每輪持續 1 秒;batchSize
:每次迭代中“批量”調用被測方法的次數。默認值為 1,若被測方法非常輕量,可以將其調大,使得每次迭代進行更多次調用,然后再累積測量。
示例日志:
# Warmup: 3 iterations, 1 s each # Warmup Iteration 1: 0.281 ops/ns # Warmup Iteration 2: 0.376 ops/ns # Warmup Iteration 3: 0.483 ops/ns
這里第 1 輪預熱的吞吐:0.281 操作/納秒 ,第 2 輪:0.376 操作/納秒,以此類推。預熱結果不會被記錄到最終輸出。
實踐建議:
- 如果方法執行非常快(例如幾十、幾百納秒),建議將
batchSize
設置為幾百或幾千,以避免單次測量粒度過小而受操作系統調度影響。 - 在分布式服務發布過程中,往往要先做請求“容量預熱”與“灰度放量”。這種預熱與 JMH 里的
@Warmup
邏輯相似,都是為了讓服務或方法進入最優狀態。
3.2 @Measurement
@Measurement(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS,batchSize = 1 // 可選
)
- 用途:配置正式測量階段的迭代輪數與持續時長,被測方法在每輪測量中的平均耗時或吞吐將被收集。
- 參數與 @Warmup 相同:區別就在于,這里的結果會被統計并輸出。
示例日志:
# Measurement: 5 iterations, 1 s each Iteration 1: 1646.000 ns/op Iteration 2: 1243.000 ns/op Iteration 3: 1273.000 ns/op Iteration 4: 1395.000 ns/op Iteration 5: 1423.000 ns/op
表示測量階段共 5 輪,每輪持續 1 秒。測量數據是每輪的平均耗時(納秒/操作)。
實踐建議:
- 在正式測量時,務必確保測試環境穩定:關閉無關進程、保證 CPU 頻率固定、網絡與磁盤 I/O 低干擾等;
- 測試結束后,關注平均值之外的最小/最大/標準差,以了解代碼在不同時刻是否存在抖動。
3.3 @BenchmarkMode
@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
-
用途:指定 JMH 在測量階段“統計哪種指標”,可同時指定多個模式。
-
常見模式(Mode)解釋:
Throughput
:吞吐量(operations per time unit),例如 “ops/ms” 表示每毫秒的調用次數,相當于 QPS。AverageTime
:平均耗時(time per operation),例如 “ns/op” 表示每次調用平均耗時納秒。SampleTime
:隨機采樣模式,會在隨機時間間隔抓取一次調用耗時分布,用于統計TP90/TP99
等百分位延遲指標。SingleShotTime
:單次執行耗時(適合測試一次性初始化、靜態塊、Cold Start 等);All
:同時統計上述所有模式并在輸出中顯示。
示例輸出:
Result "com.example.BenchmarkTest.shift":2.068 ±(99.9%) 0.038 ns/op [Average](min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010CI (99.9%): [2.030, 2.106] (assumes normal distribution) Benchmark Mode Cnt Score Error Units BenchmarkTest.div avgt 5 2.072 ± 0.053 ns/op BenchmarkTest.shift avgt 5 2.068 ± 0.038 ns/op
這里
avgt
表示 AverageTime 模式,測量了 5 輪后,shift() 的平均耗時約為 2.068ns,誤差范圍 ±0.038ns。
實踐建議:
- 若關注“總體吞吐(QPS)”,請選擇
Throughput
模式;若關注“平均延遲”,則用AverageTime
; - 在多數微基準測試中,僅關注
AverageTime
即可;如需延遲分布,可切換到SampleTime
并再結合百分位統計。
3.4 @OutputTimeUnit
@OutputTimeUnit(TimeUnit.MILLISECONDS)
-
用途:指定最終輸出結果的“時間單位”,可用在類或方法級別。常見單位:
TimeUnit.SECONDS
、MILLISECONDS
、MICROSECONDS
、NANOSECONDS
。 -
示例:
- 當
@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
,最終結果中Score
單位會顯示為ops/ms
; - 當
@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
,則結果單位為ops/s
; - 當
@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
,則結果單位為ns/op
。
- 當
示例輸出(吞吐模式,單位為 ops/ms):
Benchmark Mode Cnt Score Error Units BenchmarkTest.div thrpt 5 482999.685 ± 6415.832 ops/ms BenchmarkTest.shift thrpt 5 480599.263 ± 20752.609 ops/ms
實踐建議:
- 根據待測方法耗時大小,選擇合適的單位:若方法耗時在幾百納秒級別,用納秒;若耗時在微秒~毫秒,用
MICROSECONDS
或MILLISECONDS
;
3.5 @Fork
@Fork(value = 1, jvmArgsAppend = {"-Xmx2048m", "-server", "-XX:+AggressiveOpts"})
-
用途:指定測量時要 fork 出多少個子進程來執行完整的 Warmup + Measurement 流程,以及向每個子進程傳入哪些 JVM 參數。
-
參數說明:
value
:子進程數量,大于等于 1;jvmArgsAppend
:為子進程 JVM 增加額外參數(如-Xmx2g
、-XX:+UseG1GC
、-XX:+AggressiveOpts
等)。
-
注意:
- 每個子進程都會重新加載類、重新分配堆空間,并與主進程通過 IPC 通信或 Socket 匯報結果;
- 子進程數量過多,會延長測試時間,但能降低單個子進程偶發干擾對最終結果的影響。
日志示例:
# Fork: 1 of 1 # Warmup: 3 iterations, 1 s each # Measurement: 5 iterations, 1 s each
實踐建議:
- 推薦至少
@Fork(1)
;若想更多保障結果穩定,可設置為@Fork(3)
; - 在高性能機器上,可以結合
jvmArgsAppend
調整最大堆大小、GC 策略等,以與生產環境保持一致。
3.6 @Threads
@Threads(2)
-
用途:指定每個子進程內并發運行的線程數;
-
可選值:
- 固定整數,如
@Threads(1)
表示單線程測試; - 特殊值
Threads.MAX
,表示創建與 CPU 核數相等的線程數;
- 固定整數,如
注意:
- 過多線程會導致操作系統上下文切換增加,使得單線程測量結果難以辨別。
- 過少線程無法模擬并發場景下的“線程競爭成本”。
3.7 @Group 與 @GroupThreads
@State(Scope.Group)
@Group("myGroup")
@GroupThreads(3)
-
用途:將多個
@Benchmark
方法歸為一組(Group),并在該組內部以特定線程數并發調用各方法。常用于測試“多個方法之間的競爭”或“讀寫混合場景”。 -
示例:
@State(Scope.Group) public class MyBenchmark {@Param({"10", "100"})public int size;@Benchmark@Group("readWrite")@GroupThreads(3)public void read() {// 讀邏輯,3 個線程并發地執行 read()}@Benchmark@Group("readWrite")@GroupThreads(1)public void write() {// 寫邏輯,1 個線程并發地執行 write()} }
- 上例中,總共有 4 個線程并發:3 個線程執行
read()
,1 個線程執行write()
,它們會對同一份共享狀態(因為@State(Scope.Group)
)進行交互。
- 上例中,總共有 4 個線程并發:3 個線程執行
實踐場景:
- 測試讀寫分離緩存場景;
- 測試消息隊列生產者/消費者模型;
- 評估并發數據結構(如
ConcurrentHashMap
)在不同線程配比下的吞吐。
3.8 @State
@State(Scope.Thread)
public class MyState {// 類字段可在 benchmark 方法中直接使用
}
-
用途:聲明一個“狀態類”,其字段可在被測
@Benchmark
方法中共享或隔離。必須加在類上,否則無法運行。 -
Scope 可選值:
Scope.Benchmark
:一個類實例在所有線程、所有輪次共用;適合存放全局只讀數據;Scope.Thread
:每個線程都會有自己單獨的狀態實例,彼此互不影響;適合存放線程本地變量,避免并發沖突;Scope.Group
:在帶@Group
注解的場景下,同一個組內的線程共享一個狀態實例。
示例:
@State(Scope.Thread)
public class JMHSample_04_DefaultState {double x = Math.PI;@Benchmarkpublic void measure() {x++;}
}
Scope.Thread
意味著每個線程都有自己的x
,互不干擾;- 如果改為
Scope.Benchmark
,那么所有線程都使用同一個x
,競態寫可能會導致測量結果不穩定。
3.9 @Setup 與 @TearDown
@Setup(Level.Trial)
public void init() {// 只在 Fork 子進程啟動后、預熱前執行一次,進行全局初始化
}@TearDown(Level.Trial)
public void cleanup() {// 在同一個子進程所有測量結束后執行一次,用于回收資源
}
-
用途:定義在不同“生命周期”階段執行的初始化與清理操作,類似于 JUnit 的
@BeforeClass
/@AfterClass
。 -
Level 可選值:
Trial
(默認):在每個 Fork 子進程內部,只執行一次;Iteration
:在每輪預熱或測量開始前執行一次;Invocation
:在每次被測方法調用前執行,粒度最細,但開銷極大,通常不推薦使用。
示例:
@State(Scope.Benchmark)
public class MyBenchmark {private Connection conn;@Setup(Level.Trial)public void initConnection() {conn = DriverManager.getConnection(...);}@TearDown(Level.Trial)public void closeConnection() {conn.close();}@Benchmarkpublic void testQuery() {// 使用 conn 進行一次 database query}
}
- 在每個子進程啟動并完成 JIT 編譯后,
initConnection()
只執行一次;所有輪次共用同一個連接; - 測量結束后,
closeConnection()
執行一次,用于關閉資源。
3.10 @Param
@State(Scope.Benchmark)
public class JMHSample_27_Params {@Param({"1", "31", "65", "101", "103"})public int arg;@Param({"0", "1", "2", "4", "8", "16", "32"})public int certainty;@Benchmarkpublic boolean bench() {return BigInteger.valueOf(arg).isProbablePrime(certainty);}
}
- 用途:為字段提供一組“離散參數值”,JMH 會自動對所有參數組合進行笛卡爾積測試,統計每種參數組合下的測量結果。
- 注意:如果參數組合過多(例如兩個字段各 10 個取值,總計 100 個組合),測試需要同時針對 100 種組合分別執行多輪預熱與測量,耗時會很長。
示例輸出片段:
# JMH version: 1.23
# VM version: JDK 11.0.5, Java HotSpot(TM) 64-Bit Server VM, 11.0.5+10
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Param: arg = 1, certainty = 0
Benchmark Mode Cnt Score Error Units
JMHSample_27_Params.bench avgt 5 0.123 ± 0.005 ns/op
JMHSample_27_Params.bench avgt 5 0.130 ± 0.007 ns/op
...
# Param: arg = 65, certainty = 16
JMHSample_27_Params.bench avgt 5 59.456 ± 1.234 ns/op
...
- 每對
(arg, certainty)
配置都會生成一組測量行。
3.11 @CompilerControl
@CompilerControl(CompilerControl.Mode.INLINE)
public void hotMethod() {// ...
}@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void noInlineMethod() {// ...
}
-
用途:在 JMH 測試中,通過注解強制或禁止 JIT 編譯器對某方法進行內聯或編譯。常見模式:
INLINE
:強制將該方法內聯到調用方,消除方法調用開銷;DONT_INLINE
:禁止內聯,使得方法調用成本不被隱藏;EXCLUDE
:完全禁止 JIT 對該方法進行編譯,始終解釋執行。
示例場景:
- 若要對比“手動位移運算 vs 除法運算”在“是否被內聯”前后的性能差異,可使用
@CompilerControl
強制內聯或禁止內聯; - 分析 getter/setter 方法是否被 JIT 內聯,并測量其對調用鏈整體耗時的影響。
4. 示例代碼與分析
下面給出一個完整的 JMH 基準測試示例,比較“位移運算”和“除法運算”在相同循環次數下的性能差異(shift vs div):
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.Throughput) // 統計吞吐量
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 吞吐以“ops/ms”輸出
@State(Scope.Thread) // 每個線程一個獨立實例
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 3 輪,每輪 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 測量 5 輪,每輪 1s
@Fork(1) // 每次只啟用 1 個子進程
@Threads(2) // 每個子進程內使用 2 個并發線程
public class BenchmarkTest {@Benchmarkpublic long shift() {long t = 455565655225562L;long a = 0;for (int i = 0; i < 1000; i++) {a = t >> 30;}return a;}@Benchmarkpublic long div() {long t = 455565655225562L;long a = 0;for (int i = 0; i < 1000; i++) {a = t / 1024 / 1024 / 1024;}return a;}public static void main(String[] args) throws Exception {Options opts = new OptionsBuilder().include(BenchmarkTest.class.getSimpleName()) // 包含當前測試類.resultFormat(ResultFormatType.JSON) // 輸出 JSON 格式.build();new Runner(opts).run();}
}
4.1 關鍵點解讀
-
@BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS)
- 測量每毫秒的“操作次數”(ops/ms),適合快速計算操作的吞吐差異;
- 若換成
@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
,則測量每次調用的平均耗時(ns/op)。
-
@State(Scope.Thread)
- 每個線程都有自己的實例,此測試中兩個線程并行執行各自的
shift()
與div()
,互不干擾; - 若改為
Scope.Benchmark
,則同一實例被兩個線程共享,此處沒有共享字段,不會影響結果;
- 每個線程都有自己的實例,此測試中兩個線程并行執行各自的
-
循環內部寫死常量 vs 變量
t >> 30
與t / 1024 / 1024 / 1024
在 JIT 優化后可能都會被內聯、常量折疊;- 但在真實業務中,如果 t 來自方法參數或對象字段,則 JIT 可能無法在編譯期完全優化,此處作為示例用于對比原始運算開銷。
-
Main 方法中的 OptionsBuilder
include(...)
:表示只運行名稱匹配當前類名的基準;resultFormat(ResultFormatType.JSON)
:讓輸出結果為 JSON 格式,便于后續二次處理,如可視化;
4.2 運行與結果示例
假設用 JDK 11 運行該測試,控制臺會打印類似:
# JMH version: 1.23
# VM version: JDK 11.0.5, Java HotSpot(TM) 64-Bit Server VM, 11.0.5+10
# Warmup: 3 iterations, 1 s each
# Warmup Iteration 1: 35.000 ops/ms
# Warmup Iteration 2: 45.000 ops/ms
# Warmup Iteration 3: 50.000 ops/ms
# Measurement: 5 iterations, 1 s each
# Fork: 1 of 1
# Threads: 2 threads
# Benchmark: com.example.BenchmarkTest.shiftIteration 1: 480000.000 ops/ms
Iteration 2: 485000.000 ops/ms
Iteration 3: 482500.000 ops/ms
Iteration 4: 484000.000 ops/ms
Iteration 5: 483200.000 ops/msResult "com.example.BenchmarkTest.shift":482740.000 ±(99.9%) 5000.000 ops/ms [Thrpt]
# Benchmark: com.example.BenchmarkTest.divIteration 1: 478000.000 ops/ms
Iteration 2: 482000.000 ops/ms
Iteration 3: 480500.000 ops/ms
Iteration 4: 481000.000 ops/ms
Iteration 5: 480800.000 ops/msResult "com.example.BenchmarkTest.div":480460.000 ±(99.9%) 4500.000 ops/ms [Thrpt]
從示例結果可見:
-
shift()
的平均吞吐約為 482,740 ops/ms,而div()
為 480,460 ops/ms; -
兩者吞吐非常接近,但依舊可以通過
CI (99.9%)
判斷差異; -
若將模式改為
AverageTime
,則會輸出每次平均耗時(ns/op),如下:Benchmark Mode Cnt Score Error Units BenchmarkTest.div avgt 5 2.072 ± 0.053 ns/op BenchmarkTest.shift avgt 5 2.068 ± 0.038 ns/op
5. 結果可視化與二次處理
JMH 除了支持在控制臺輸出之外,還能將測量結果導出為通用格式,以便用第三方工具生成可視化圖表。
5.1 支持的輸出格式
在 OptionsBuilder
中可配置 .resultFormat(ResultFormatType.X)
,JMH 默認支持五種格式:
TEXT
:純文本格式,適合查看簡單日志;CSV
:逗號分隔值,可直接用 Excel 或其他工具打開;SCSV
:分號分隔值,與某些地區的 CSV 兼容;JSON
:JSON 結構,適合與腳本或可視化工具對接;LATEX
:可導入 LaTeX 文檔,用于學術論文排版。
示例:
Options opts = new OptionsBuilder().include(BenchmarkTest.class.getSimpleName()).resultFormat(ResultFormatType.CSV) // 輸出成 CSV 文件.result("benchmark_results.csv").build();
new Runner(opts).run();
- 運行完成后,會在當前目錄生成
benchmark_results.csv
,內容包含:Benchmark 名稱、Mode、Threads、Fork、Param 值、Score、Error、Units 等列;
5.2 可視化工具推薦
-
JMH Visualizer(在線工具)
- 網站地址: https://jmh.morethan.io/
- 使用方式:將 JMH 導出的 JSON 文件上傳,可生成不同參數組合下各模式的條形圖或折線圖;
- 缺點:交互需要鼠標懸浮展示信息,對于大數據集可視化體驗一般。
-
JMH Visual Chart(開源桌面/網頁工具)
- GitHub 地址: https://github.com/PengRong/visual-jmh
- 特色:支持 JSON/CSV 導入,自動識別不同 Benchmark 與參數,將數據按照“吞吐 vs 參數”或“延遲 vs 參數”等方式可視化;
- 使用時將生成的
benchmark_results.json
拖入工具即可自動繪制。
-
meta-chart(通用在線圖表生成)
- 網站地址: https://www.meta-chart.com/
- 使用方式:先將 JMH 導出的 CSV 文件加載到 Excel,篩選出需要展示的數據(如某個方法在不同線程數下的吞吐),再將表格復制到 meta-chart,用柱狀圖、折線圖或散點圖進行展示;
- 優點:靈活度高,可自定義圖例、坐標軸、注釋等;
-
Jenkins & CI 插件
- 如果將 JMH 測試流程集成到 Jenkins 等 CI 平臺,可使用社區插件(如 “Benchmark Parser Plugin”)直接把 CSV/JSON 結果展示在 Jenkins 界面,作為每日構建的性能回歸圖表。
示例可視化效果(結合文字說明,省略具體截圖):
條形圖展示
shift()
vsdiv()
在吞吐模式下的對比:
- 橫軸:操作名稱(shift、div);
- 縱軸:
ops/ms
;折線圖展示
@Param
不同參數組合下的AverageTime
:
- 橫軸:參數值(如 arg=1、31、65、101、103);
- 縱軸:
ns/op
;- 不同曲線表示不同
certainty
值。
小結
-
為何要使用 JMH 而非簡單手工計時:JMH 能自動完成預熱、迭代、進程隔離與統計分析,結果更穩定且精度高;
-
JMH 執行流程:
- Fork:多進程隔離環境,避免單 JVM 環境干擾;
- Warmup:驅動 JIT 編譯與方法內聯,進入穩態;
- Measurement:正式測量階段,統計吞吐或延遲;
- Result 匯總:收集子進程數據并輸出;
-
關鍵注解詳解:
- @Warmup / @Measurement:配置預熱與測量輪次、時長;
- @BenchmarkMode / @OutputTimeUnit:定義統計模式與輸出單位;
- @Fork / @Threads:控制子進程數量與線程并發數;
- @State:聲明狀態作用域(Benchmark/Thread/Group);
- @Setup / @TearDown:初始化與清理動作的時機;
- @Param:批量測試不同參數組合;
- @CompilerControl:強制或禁止 JIT 內聯與編譯;
-
示例分析:以
shift()
vsdiv()
的對比測試,演示完整的 JMH 配置與測量結果格式; -
結果可視化與二次加工:
- 支持的導出格式:TEXT、CSV、SCSV、JSON、LATEX;
- 常用可視化工具:JMH Visualizer、JMH Visual Chart、meta-chart 以及 Jenkins 插件;
借助 JMH,可以對“熱點代碼”或“關鍵算法”進行精準的微基準測試,量化優化前后的性能提升,避免盲目優化或主觀判斷。