JMH,全稱 Java Microbenchmark Harness (微基准測試框架)


在日常開發中,我們對一些代碼的調用或者工具的使用會存在多種選擇方式,在不確定他們性能的時候,我們首先想要做的就是去測量它。大多數時候,我們會簡單的采用多次計數的方式來測量,來看這個方法的總耗時。

但是,如果熟悉JVM類加載機制的話,應該知道JVM默認的執行模式是JIT編譯與解釋混合執行。JVM通過熱點代碼統計分析,識別高頻方法的調用、循環體、公共模塊等,基於JIT動態編譯技術,會將熱點代碼轉換成機器碼,直接交給CPU執行。

也就是說,JVM會不斷的進行編譯優化,這就使得很難確定重復多少次才能得到一個穩定的測試結果?所以,很多有經驗的同學會在測試代碼前寫一段預熱的邏輯。

JMH,全稱 Java Microbenchmark Harness (微基准測試框架),是專門用於Java代碼微基准測試的一套測試工具API,是由 OpenJDK/Oracle 官方發布的工具。何謂 Micro Benchmark 呢?簡單地說就是在 method 層面上的 benchmark,精度可以精確到微秒級。

Java的基准測試需要注意的幾個點:

  • 測試前需要預熱。
  • 防止無用代碼進入測試方法中。
  • 並發測試。
  • 測試結果呈現。

JMH的使用場景:

  1. 定量分析某個熱點函數的優化效果
  2. 想定量地知道某個函數需要執行多長時間,以及執行時間和輸入變量的相關性
  3. 對比一個函數的多種實現方式

 

demo

依賴:

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>${jmh.version}</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>${jmh.version}</version>
    <scope>provided</scope>
</dependency>

這里我以測試LinkedList 通過index 方式迭代和foreach 方式迭代的性能差距為例子,編寫測試類

@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.SECONDS)
@Threads(Threads.MAX)
public class LinkedListIterationBenchMark {

    private static final int SIZE = 10000;

    private List<String> list = new LinkedList<>();

    @Setup
    public void setUp() {
        for (int i = 0; i < SIZE; i++) {
            list.add(String.valueOf(i));
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public void forIndexIterate() {
        for (int i = 0; i < list.size(); i++) {
            list.get(i);
            System.out.print("");
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public void forEachIterate() {
        for (String s : list) {
            System.out.print("");
        }
    }
}

IDE中測試:

/**
 * 僅限於IDE中運行
 * 命令行模式 則是 build 然后 java -jar 啟動
 * <p>
 * 1. 這是benchmark 啟動的入口
 * 2. 這里同時還完成了JMH測試的一些配置工作
 * 3. 默認場景下,JMH會去找尋標注了@Benchmark的方法,可以通過include和exclude兩個方法來完成包含以及排除的語義
 */
public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
            .include(LinkedListIterationBenchMark.class.getSimpleName()) // 可以用方法名,也可以用XXX.class.getSimpleName()
            .forks(3)//  forks(3)指的是做3輪測試,因為一次測試無法有效的代表結果,所以通過3輪測試較為全面的測試,而每一輪都是先預熱,再正式計量。
            .warmupIterations(2)// 預熱2輪
            .measurementIterations(2)// 代表正式計量測試做2輪,而每次都是先執行完預熱再執行正式計量, 內容都是調用標注了@Benchmark的代碼。
            .output("D:/Benchmark.log")
            .build();

    new Runner(opt).run();
}

測試結果:

Benchmark                                      Mode  Cnt    Score   Error  Units
LinkedListIterationBenchMark.forEachIterate   thrpt    2  481.343          ops/s
LinkedListIterationBenchMark.forIndexIterate  thrpt    2   65.249          ops/s

 

 

注解介紹

@BenchmarkMode

微基准測試類型。JMH 提供了以下幾種類型進行支持:

類型 描述
Throughput 每段時間執行的次數,一般是秒
AverageTime 平均時間,每次操作的平均耗時
SampleTime 在測試中,隨機進行采樣執行的時間
SingleShotTime 在每次執行中計算耗時
All 所有模式

 

@Warmup

這個單詞的意思就是預熱,iterations = 3就是指預熱輪數。

@Measurement

正式度量計算的輪數。

  • iterations 進行測試的輪次
  • time 每輪進行的時長
  • timeUnit時長單位

@Threads

每個進程中的測試線程。

@Fork

進行 fork 的次數。如果 fork 數是3的話,則 JMH 會 fork 出3個進程來進行測試。

@OutputTimeUnit

基准測試結果的時間類型。一般選擇秒、毫秒、微秒。

@Benchmark

方法級注解,表示該方法是需要進行 benchmark 的對象,用法和 JUnit 的 @Test 類似。

@Param

屬性級注解,@Param 可以用來指定某項參數的多種情況。特別適合用來測試一個函數在不同的參數輸入的情況下的性能。

@Setup

方法級注解,這個注解的作用就是我們需要在測試之前進行一些准備工作,比如對一些數據的初始化之類的。

@TearDown

方法級注解,這個注解的作用就是我們需要在測試之后進行一些結束工作,比如關閉線程池,數據庫連接等的,主要用於資源的回收等。

@State

當使用@Setup參數的時候,必須在類上加這個參數,不然會提示無法運行。

就比如我上面的例子中,就必須設置state

State 用於聲明某個類是一個“狀態”,然后接受一個 Scope 參數用來表示該狀態的共享范圍。因為很多 benchmark 會需要一些表示狀態的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數里。Scope 主要分為三種。

  1. Thread: 該狀態為每個線程獨享。
  2. Group: 該狀態為同一個組里面所有線程共享。
  3. Benchmark: 該狀態在所有線程間共享。

 

 

 

參考:juejin.cn/post/6844903936869007368

更多的example可以參考官方給出的JMH samples (https://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM