性能優化 - 工具篇:基準測試 JMH

文章目錄

  • 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診斷手段

性能優化 - 工具篇:常用的性能測試工具


  1. 引言:手工計時的局限,說明 JMH 的必要性與優勢;
  2. JMH 簡介:工具背景、引入方式(JDK 12 內置與 Maven 依賴);
  3. JMH 執行流程:Fork、Threads、Warmup、Measurement 四階段說明;
  4. 關鍵注解詳解:
    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:強制內聯與編譯控制;
  5. 示例代碼分析:對比 shift 與 div 兩種寫法的基準測試;
  6. 結果可視化:JMH 支持的五種輸出格式,常用 CSV/JSON 導出;以及三款可視化工具(JMH Visualizer、JMH Visual Chart、meta-chart);
  7. 小結:JMH 在熱點代碼驗證與專項微基準測試中的應用價值;

引言

在以往的項目開發與性能調優中,我們經常會寫類似如下的簡單計時代碼:

long start = System.currentTimeMillis();
// logic
long cost = System.currentTimeMillis() - start;
System.out.println("Logic cost : " + cost);

然而,這種方式往往并不準確:

  1. JIT 編譯與方法內聯

    • 在 HotSpot JVM 中,編譯器會對“熱點方法”進行 JIT 編譯并嘗試內聯;
    • 如果僅執行一次計時,沒有經過充分“預熱”,那就是測的是“解釋執行”甚至是“部分編譯前”的性能。
  2. 測量誤差與噪聲干擾

    • 操作系統調度、垃圾回收、上下文切換、CPU 頻率變動等因素,都可能導致 System.currentTimeMillis() 級別的測量出現幾十毫秒甚至幾百毫秒的抖動;
    • 對于微基準測試(microbenchmark),通常期望測量精度達到“納秒”級別,需要靠更專業的工具來保證。
  3. 重復測量與結果收斂

    • 如果邏輯執行速度非常快,僅靠一次或少量循環,很難區分不同實現間的微小差距;
    • 必須反復多次執行、結合預熱階段,才能進入“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-corejmh-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 或共享內存傳回主進程,最后統一匯總、統計并輸出。整個流程大致如下:

  1. Fork(進程隔離):JMH 啟動指定數量的子進程,每個子進程都運行相同的測試。
  2. Warmup(預熱):在每個子進程中,對目標方法執行若干輪預熱(可配置輪數和時長),驅動 JIT 編譯與內聯優化。
  3. Measurement(正式測量):預熱結束后,進入正式測量階段,同樣執行可配置輪數和時長,收集方法執行耗時或吞吐統計。
  4. 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)

下面將結合這段日志逐項說明流程與參數含義:

  1. Warmup(預熱)

    • 例中配置為 3 iterations, 1 s each,表示在每個 Fork 進程中,先進行 3 輪預熱,每輪持續 1 秒,將預熱階段測得的吞吐(ops/ns)丟棄,不納入最終統計;
    • 預熱結束后,JMH 會自動等待 JIT 完成優化,使得被測方法進入“穩態”(steady-state)狀態,從而讓 Measurement 階段結果更準確。
  2. Measurement(測量)

    • 配置為 5 iterations, 1 s each,意味著正式測量階段同樣分 5 輪,每輪持續 1 秒,將每輪測量結果記錄到輸出;
    • 日志中每行 Iteration X: YYY ns/op,即表示第 X 輪的平均耗時(以納秒為單位);
    • JMH 會將所有輪次的測量結果匯總,計算整體的平均值(avg)、標準差(stdev)、95% 或 99.9% 置信區間(CI)等統計指標。
  3. 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 的參數啟動,更精細地控制測量環境。

  4. 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.SECONDSMILLISECONDSMICROSECONDSNANOSECONDS

  • 示例

    • @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

實踐建議

  • 根據待測方法耗時大小,選擇合適的單位:若方法耗時在幾百納秒級別,用納秒;若耗時在微秒~毫秒,用 MICROSECONDSMILLISECONDS

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))進行交互。

實踐場景

  • 測試讀寫分離緩存場景;
  • 測試消息隊列生產者/消費者模型;
  • 評估并發數據結構(如 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 關鍵點解讀

  1. @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS)

    • 測量每毫秒的“操作次數”(ops/ms),適合快速計算操作的吞吐差異;
    • 若換成 @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS),則測量每次調用的平均耗時(ns/op)。
  2. @State(Scope.Thread)

    • 每個線程都有自己的實例,此測試中兩個線程并行執行各自的 shift()div(),互不干擾;
    • 若改為 Scope.Benchmark,則同一實例被兩個線程共享,此處沒有共享字段,不會影響結果;
  3. 循環內部寫死常量 vs 變量

    • t >> 30t / 1024 / 1024 / 1024 在 JIT 優化后可能都會被內聯、常量折疊;
    • 但在真實業務中,如果 t 來自方法參數或對象字段,則 JIT 可能無法在編譯期完全優化,此處作為示例用于對比原始運算開銷。
  4. 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 默認支持五種格式:

  1. TEXT:純文本格式,適合查看簡單日志;
  2. CSV:逗號分隔值,可直接用 Excel 或其他工具打開;
  3. SCSV:分號分隔值,與某些地區的 CSV 兼容;
  4. JSON:JSON 結構,適合與腳本或可視化工具對接;
  5. 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 可視化工具推薦

  1. JMH Visualizer(在線工具)

    • 網站地址: https://jmh.morethan.io/
    • 使用方式:將 JMH 導出的 JSON 文件上傳,可生成不同參數組合下各模式的條形圖或折線圖;
    • 缺點:交互需要鼠標懸浮展示信息,對于大數據集可視化體驗一般。
  2. JMH Visual Chart(開源桌面/網頁工具)

    • GitHub 地址: https://github.com/PengRong/visual-jmh
    • 特色:支持 JSON/CSV 導入,自動識別不同 Benchmark 與參數,將數據按照“吞吐 vs 參數”或“延遲 vs 參數”等方式可視化;
    • 使用時將生成的 benchmark_results.json 拖入工具即可自動繪制。
  3. meta-chart(通用在線圖表生成)

    • 網站地址: https://www.meta-chart.com/
    • 使用方式:先將 JMH 導出的 CSV 文件加載到 Excel,篩選出需要展示的數據(如某個方法在不同線程數下的吞吐),再將表格復制到 meta-chart,用柱狀圖、折線圖或散點圖進行展示;
    • 優點:靈活度高,可自定義圖例、坐標軸、注釋等;
  4. Jenkins & CI 插件

    • 如果將 JMH 測試流程集成到 Jenkins 等 CI 平臺,可使用社區插件(如 “Benchmark Parser Plugin”)直接把 CSV/JSON 結果展示在 Jenkins 界面,作為每日構建的性能回歸圖表。

示例可視化效果(結合文字說明,省略具體截圖):

  • 條形圖展示 shift() vs div() 在吞吐模式下的對比:

    • 橫軸:操作名稱(shift、div);
    • 縱軸:ops/ms
  • 折線圖展示 @Param 不同參數組合下的 AverageTime

    • 橫軸:參數值(如 arg=1、31、65、101、103);
    • 縱軸:ns/op
    • 不同曲線表示不同 certainty 值。

小結

  1. 為何要使用 JMH 而非簡單手工計時:JMH 能自動完成預熱、迭代、進程隔離與統計分析,結果更穩定且精度高;

  2. JMH 執行流程

    • Fork:多進程隔離環境,避免單 JVM 環境干擾;
    • Warmup:驅動 JIT 編譯與方法內聯,進入穩態;
    • Measurement:正式測量階段,統計吞吐或延遲;
    • Result 匯總:收集子進程數據并輸出;
  3. 關鍵注解詳解

    • @Warmup / @Measurement:配置預熱與測量輪次、時長;
    • @BenchmarkMode / @OutputTimeUnit:定義統計模式與輸出單位;
    • @Fork / @Threads:控制子進程數量與線程并發數;
    • @State:聲明狀態作用域(Benchmark/Thread/Group);
    • @Setup / @TearDown:初始化與清理動作的時機;
    • @Param:批量測試不同參數組合;
    • @CompilerControl:強制或禁止 JIT 內聯與編譯;
  4. 示例分析:以 shift() vs div() 的對比測試,演示完整的 JMH 配置與測量結果格式;

  5. 結果可視化與二次加工

    • 支持的導出格式:TEXT、CSV、SCSV、JSON、LATEX;
    • 常用可視化工具:JMH Visualizer、JMH Visual Chart、meta-chart 以及 Jenkins 插件;

借助 JMH,可以對“熱點代碼”或“關鍵算法”進行精準的微基準測試,量化優化前后的性能提升,避免盲目優化或主觀判斷。

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/907894.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/907894.shtml
英文地址,請注明出處:http://en.pswp.cn/news/907894.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

2025年十大AI幻燈片工具深度評測與推薦

我來告訴你一個好消息。 我們已經親自測試和對比了市面上最優秀的AI幻燈片工具&#xff0c;讓你無需再為選擇而煩惱。 得益于AI技術的飛速發展&#xff0c;如今你可以快速制作出美觀、專業的幻燈片。 這些智能平臺的功能遠不止于配色美化——它們能幫你頭腦風暴、梳理思路、…

雪花算法:分布式ID生成的優雅解決方案

一、雪花算法的核心機制與設計思想 雪花算法&#xff08;Snowflake&#xff09;是由Twitter開源的分布式ID生成算法&#xff0c;它通過巧妙的位運算設計&#xff0c;能夠在分布式系統中快速生成全局唯一且趨勢遞增的ID。 1. 基本結構 雪花算法生成的是一個64位&#xff08;lo…

第1章:走進Golang

第1章&#xff1a;走進Golang 一、Golang簡介 Go語言&#xff08;又稱Golang&#xff09;是由Google的Robert Griesemer、Rob Pike及Ken Thompson開發的一種開源編程語言。它誕生于2007年&#xff0c;2009年11月正式開源。Go語言的設計初衷是為了在不損失應用程序性能的情況下…

Higress項目解析(二):Proxy-Wasm Go SDK

3、Proxy-Wasm Go SDK Proxy-Wasm Go SDK 依賴于 tinygo&#xff0c;同時 Proxy - Wasm Go SDK 是基于 Proxy-Wasm ABI 規范使用 Go 編程語言擴展網絡代理&#xff08;例如 Envoy&#xff09;的 SDK&#xff0c;而 Proxy-Wasm ABI 定義了網絡代理和在網絡代理內部運行的 Wasm …

NVMe IP現狀掃盲

SSD優勢 與機械硬盤&#xff08;Hard Disk Driver, HDD&#xff09;相比&#xff0c;基于Flash的SSD具有更快的數據隨機訪問速度、更快的傳輸速率和更低的功耗優勢&#xff0c;已經被廣泛應用于各種計算領域和存儲系統。SSD最初遵循為HDD設計的現有主機接口協議&#xff0c;例…

`docker commit` 和 `docker save`區別

理解 docker commit 和 docker save 之間的區別對于正確管理 Docker 鏡像非常重要。讓我們詳細解釋一下這兩個命令的作用及其區別。 1. docker commit 作用&#xff1a; docker commit roop-builder roop:v1 命令的作用是基于一個正在運行的容器 roop-builder 創建一個新的鏡…

Linux內核體系結構簡析

1.Linux內核 1.1 Linux內核的任務 從技術層面講&#xff0c;內核是硬件和軟件之間的一個中間層&#xff0c;作用是將應用層序的請求傳遞給硬件&#xff0c;并充當底層驅動程序&#xff0c;對系統中的各種設備和組件進行尋址。從應用程序的角度講&#xff0c;應用程序與硬件沒有…

python爬蟲:Ruia的詳細使用(一個基于asyncio和aiohttp的異步爬蟲框架)

更多內容請見: 爬蟲和逆向教程-專欄介紹和目錄 文章目錄 一、Ruia概述1.1 Ruia介紹1.2 Ruia特點1.3 安裝Ruia1.4 使用案例二、基本使用2.1 Request 請求2.2 Response - 響應2.3 Item - 數據提取2.4 Field 提取數據2.5 Spider - 爬蟲類2.6 Middleware - 中間件三、高級功能3.1 …

網絡攻防技術二:密碼學分析

文章目錄 一、傳統密碼分析方法1、根據明文、密文等信息的掌握情況分類 2、從密碼分析途徑分類二、密碼旁路分析1、概念2、旁路分析方法三、現代密碼系統1、對稱密碼&#xff08;單密鑰&#xff09;2、公開密碼&#xff08;成對密鑰&#xff09; 四、典型對稱密碼&#xff08;單…

Linux --TCP協議實現簡單的網絡通信(中英翻譯)

一、什么是TCP協議 1.1 、TCP是傳輸層的協議&#xff0c;TCP需要連接&#xff0c;TCP是一種可靠性傳輸協議&#xff0c;TCP是面向字節流的傳輸協議&#xff1b; 二、TCPserver端的搭建 2.1、我們最終好實現的效果是 客戶端在任何時候都能連接到服務端&#xff0c;然后向服務…

pc端小卡片功能-原生JavaScript金融信息與節日日歷

代碼如下 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>金融信息與節日日歷</title><…

C語言——獲取變量所在地址(uint8和uint32的區別)

前言&#xff1a; 1.使用uint8 *的原因 在C語言中&#xff0c;獲取或操作一個4字節地址&#xff08;指針&#xff09;時使用uint8_t*&#xff08;即unsigned char*&#xff09;而不是uint32_t*&#xff0c;主要基于以下關鍵原因&#xff1a; 1.1. 避免違反嚴格別名規則&…

Python----目標檢測(《YOLOv3:AnIncrementalImprovement》和YOLO-V3的原理與網絡結構)

一、《YOLOv3:AnIncrementalImprovement》 1.1、基本信息 標題&#xff1a;YOLOv3: An Incremental Improvement 作者&#xff1a;Joseph Redmon, Ali Farhadi 機構&#xff1a;華盛頓大學&#xff08;University of Washington&#xff09; 發表時間&#xff1a;2018年 代…

50天50個小項目 (Vue3 + Tailwindcss V4) ? | Form Wave(表單label波動效果)

&#x1f4c5; 我們繼續 50 個小項目挑戰&#xff01;—— FormWave組件 倉庫地址&#xff1a;https://github.com/SunACong/50-vue-projects 項目預覽地址&#xff1a;https://50-vue-projects.vercel.app/ &#x1f3af; 組件目標 構建一個美觀、動態的登錄表單&#xff0…

【數據結構】--二叉樹--堆(上)

一、樹的概念和結構 概念&#xff1a; 樹是一種非線性的數據結構&#xff0c;他是由n(n>0)個有限結點組成一個具有層次關系的集合。其叫做樹&#xff0c;是因為他倒過來看就和一棵樹差不多&#xff0c;其實際上是根在上&#xff0c;樹枝在下的。 樹的特點&#xff1a; 1…

linux有效裁剪視頻的方式(基于ffmpeg,不改變分辨率,幀率,視頻質量,不需要三方軟件)

就是在Linux上使用OBS Studio錄制一個講座或者其他視頻&#xff0c;可能總有些時候會多錄制一段時間&#xff0c;但是如果使用剪映或者PR這樣的工具在導出的時候總需要煩惱導出的格式和參數&#xff0c;比如剪映就不支持mkv格式的導出&#xff0c;導出成mp4格式的視頻就會變得很…

SystemVerilog—Interface語法(一)

SystemVerilog中的接口&#xff08;interface&#xff09;是一種用于封裝多模塊間通信信號和協議的復合結構&#xff0c;可顯著提升代碼復用性和維護效率。其核心語法和功能如下&#xff1a; 一、接口的基本定義 1. 聲明語法 接口通過interface關鍵字定義&#xff0c;支持信…

android binder(四)binder驅動詳解

ref&#xff1a; Android10.0 Binder通信原理(五)-Binder驅動分析_binder: 1203:1453 ioctl 40046210 77004d93f4 return-CSDN博客 https://juejin.cn/post/7214342319347712057#heading-0 第6課第1節_Binder系統_驅動情景分析_數據結構_嗶哩嗶哩_bilibili

QT/c++航空返修數據智能分析系統

簡介 1、區分普通用戶和管理員 2、界面精美 3、功能豐富 4、使用cppjieba分詞分析數據 5、支持數據導入導出 6、echarts展示圖表 效果展示 演示鏈接 源碼獲取 int main(){ //非白嫖 printf("&#x1f4e1;:%S","joyfelic"); return 0; }

ToolsSet之:數值提取及批處理

ToolsSet是微軟商店中的一款包含數十種實用工具數百種細分功能的工具集合應用&#xff0c;應用基本功能介紹可以查看以下文章&#xff1a; Windows應用ToolsSet介紹https://blog.csdn.net/BinField/article/details/145898264 ToolsSet中Number菜單下的Numeric Batch是一個數…