Java基準效能測試--JMH使用介紹

Kevin.ZhangCG發表於2021-06-29

JMH是什麼

JMH是Java Microbenchmark Harness的簡稱,一個針對Java做基準測試的工具,是由開發JVM的那群人開發的。想準確的對一段程式碼做基準效能測試並不容易,因為JVM層面在編譯期、執行時對程式碼做很多優化,但是當程式碼塊處於整個系統中執行時這些優化並不一定會生效,從而產生錯誤的基準測試結果,而這個問題就是JMH要解決的。

JMH vs JMeter

JMeter可能是最常用的效能測試工具。它既支援圖形介面,也支援命令列,屬於黑盒測試的範疇,對非開發人員比較友好,上手也非常容易。圖形介面一般用於編寫、除錯測試用例,而實際的效能測試建議還是在命令列下執行。

很多場景下JMeter和JMH都可以做效能測試,但是對於嚴格意義上的基準測試來說,只有JMH才適合。JMeter的測試結果精度相對JVM較低、所以JMeter不適合於類級別的基準測試,更適合於對精度要求不高、耗時相對較長的操作。

  • JMeter測試精度差: JMeter自身框架比較重,舉個例子:使用JMH測試一個方法,平均耗時0.01ms,而使用JMeter測試的結果平均耗時20ms,相差200倍。
  • JMeter內建很多采樣器:JMeter內建了支援多種網路協議的取樣器,可以在不寫Java程式碼的情況下實現很多複雜的測試。JMeter支援叢集的方式執行,方便模擬多使用者、高併發壓力測試。

總結: JMeter適合一些相對耗時的整合功能測試,如API介面的測試。JMH適合於類或者方法的單元測試。

JMH基本用法

建立JMH專案

官方推薦為JMH基準測試建立單獨的專案,最簡單的建立JMH專案的方法就是基於maven專案原型的方式建立(如果是在windows環境下,需要對org.open.jdk.jmh這樣帶.的用雙引號包裹)。

 mvn archetype:generate
          -DinteractiveMode=false
          -DarchetypeGroupId=org.openjdk.jmh
          -DarchetypeArtifactId=jmh-java-benchmark-archetype
          -DarchetypeVersion=1.21
          -DgroupId=com.jenkov
          -DartifactId=first-benchmark
          -Dversion=1.0
可以看到生成的專案pom檔案中主要是新增了兩個jmh
的依賴和設定了maven-shade-plugin的編譯方式(負責把專案的所有依賴jar包打入到目標jar包中,與springboot的實現方式類似)。
<dependencies>
        <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>
    </dependencies>
...
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.2</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <finalName>${uberjar.name}</finalName>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>org.openjdk.jmh.Main</mainClass>
                    </transformer>
                </transformers>
                <filters>
                    <filter>
                        <!--
                            Shading signed JARs will fail without this.
                            http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
                        -->
                        <artifact>*:*</artifact>
                        <excludes>
                            <exclude>META-INF/*.SF</exclude>
                            <exclude>META-INF/*.DSA</exclude>
                            <exclude>META-INF/*.RSA</exclude>
                        </excludes>
                    </filter>
                </filters>
            </configuration>
        </execution>
    </executions>
</plugin>

生成的專案中已經包含了一個class檔案MyBenchmark.java,如下:

public class MyBenchmark {

    @Benchmark
    public void testMethod() {
        // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
        // Put your benchmark code here.
    }

}

編寫基準測試程式碼

在上面生成的MyBenchmark類的testMethod中就可以新增基準測試的java程式碼,舉例如下:測試AtomicInteger的incrementAndGet的基準效能。

public class MyBenchmark {
    static AtomicInteger integer = new AtomicInteger();

    @Benchmark
    public void testMethod() {
        // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
        // Put your benchmark code here.
        integer.incrementAndGet();
    }
}

JMH打包、執行

專案打包

mvn clean install

執行生成的目標jar包benchmark.jar:

java -jar benchmark.jar

# JMH version: 1.21
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: C:\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: <none>
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.MyBenchmark.testMethod

# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration   1: 81052462.185 ops/s
# Warmup Iteration   2: 80152956.333 ops/s
# Warmup Iteration   3: 81305026.522 ops/s
# Warmup Iteration   4: 81740215.227 ops/s
# Warmup Iteration   5: 82398485.097 ops/s
Iteration   1: 82176523.804 ops/s
Iteration   2: 81818881.730 ops/s
Iteration   3: 82812749.807 ops/s
Iteration   4: 82406672.531 ops/s
Iteration   5: 74270344.512 ops/s


Result "org.sample.MyBenchmark.testMethod":
  80697034.477 ±(99.9%) 13903555.960 ops/s [Average]
  (min, avg, max) = (74270344.512, 80697034.477, 82812749.807), stdev = 3610709.330
  CI (99.9%): [66793478.517, 94600590.437] (assumes normal distribution)


# Run complete. Total time: 00:01:41

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
MyBenchmark.testMethod  thrpt    5  80697034.477 ± 13903555.960  ops/s

從上面的日誌我們大致可以瞭解到 JMH的基準測試主要經歷了下面幾個過程:

  1. 列印本次測試的配置,warmup:5輪;measurement:5輪;每輪:10s;啟動1個執行緒做測試;基準測試指標:吞吐量(throughput,單位是s);測試方法MyBenchmark.testMethod
  2. 啟動一個JVM程式做基準測試(也可以設定啟動多個程式,減少隨機因素的誤差影響)
  3. 在JVM程式中先執行了5輪的預熱(warmup),每輪10s,總共50s的預熱時間。預熱的資料不作為基準測試的參考。
  4. 測試了5輪,每輪10s,總共50s的測試時間
  5. 彙總測試資料、生成結果報表。最終結論是吞吐量(80697034.477 ±13903555.960 ops/s),其中80697034.477 是結果,13903555.960是誤差範圍。

JMH與Springboot

在對Springboot專案做JMH基準測試時可能會因為maven-shade-plugin外掛的問題打包報錯,需要在JMH的maven-shade-plugin的外掛配置中新增id即可。專案的pom可能如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.7.RELEASE</version>
        <relativePath/>
    </parent>
...
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.2</version>
    <executions>
        <execution>
            <!-- 需要在此處新增一個id標籤,否則mvn package時會報錯 -->
            <id>shade-all-dependency-jar</id>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                ...
            </configuration>
        </execution>
    </executions>
</plugin>
...
</project>

在測試程式碼中正常基於SpringBootApplication構建ConfigurableApplicationContext從而獲取bean的方式獲取物件測試即可。

public class StringRedisTemplateBenchmark  {
    StringRedisTemplate redisTemplate;
            
    @Setup(Level.Trial)
    public void setUp() {
        redisTemplate = SpringApplication.run(SpringBootApplicationClass.class).getBean(StringRedisTemplate.class);
    }
    
    @Benchmark
    public void testGet() {
        redisTemplate.opsForValue().get("testkey");
    }
}

@SpringBootApplication
public class SpringBootApplicationClass {

}

application.properties

lettuce.pool.maxTotal=50
lettuce.pool.maxIdle=10
lettuce.pool.minIdle=0

lettuce.sentinel.master=mymaster
lettuce.sentinel.nodes=10.xx.xx.xx:26379,10.xx.xx.xx:26379
lettuce.password=xxxxxx

JMH註解

JMH測試的相關配置大多是通過註解的方式體現的。具體每個註解的使用例項也可以參考官網http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

JMH Benchmark Modes

JMH benchmark支援如下幾種測試模式:

  • Throughput: 吞吐量,測試每秒可以執行操作的次數
  • Average Time: 平均耗時,測試單次操作的平均耗時
  • Sample Time:取樣耗時,測試單次操作的耗時,包括最大、最小耗時,已經百分位耗時等
  • Single Shot Time: 只計算一次的耗時,一般用來測試冷啟動的效能(不設定JVM預熱)
  • All: 測試上面的所有指標

預設的benchmark mode是Throughput,可以通過註解的方式設定BenchmarkMode,註解支援放在類或方法上。如下所示設定了Throughput和SampleTime兩個Benchmark mode。

@BenchmarkMode({Mode.Throughput, Mode.SampleTime})
public class MyBenchmark {
    static AtomicInteger integer = new AtomicInteger();

    @Benchmark
    public void testMethod() {
        // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
        // Put your benchmark code here.
        integer.incrementAndGet();
    }
}

Benchmark Time Units

JMH支援設定列印基準測試結果的時間單位,通過@OutputTimeUnit註解的方式設定。

@OutputTimeUnit(TimeUnit.SECONDS)
public class MyBenchmark {
    static AtomicInteger integer = new AtomicInteger();

    @Benchmark
    public void testMethod() {
        integer.incrementAndGet();
    }

}

Benchmark State

有時候我們在做基準測試的時候會需要使用一些變數、欄位,@State註解是用來配置這些變數的生命週期,@State註解可以放在類上,然後在基準測試方法中可以通過引數的方式把該類物件作為引數使用。@State支援的生命週期型別:

  • Benchmark: 整個基準測試的生命週期,多個執行緒共用同一份例項物件。該類內部的@Setup @TearDown註解的方法可能會被任一個執行緒執行,但是隻會執行一次。
  • Group: 每一個Group內部共享同一個例項,需要配合@Group @GroupThread使用。該類內部的@Setup @TearDown註解的方法可能會該Group內的任一個執行緒執行,但是隻會執行一次。
  • Thread:每個執行緒的例項都是不同的、唯一的。該類內部的@Setup @TearDown註解的方法只會被當前執行緒執行,而且只會執行一次。

被@State標示的類必須滿足如下兩個要求:

  • 類必須是public的
  • 必須有無參建構函式

State Object @Setup @TearDown

在@Scope註解標示的類的方法上可以新增@Setup和@TearDwon註解。@Setup:用來標示在Benchmark方法使用State物件之前需要執行的操作。@TearDown:用來標示在Benchmark方法之後需要對State物件執行的操作。
如下示例:

@OutputTimeUnit(TimeUnit.SECONDS)
public class MyBenchmark {
    
    @Benchmark
    public void testMethod(TestAddAndGetState state) {
        state.getInteger().incrementAndGet();
    }

    @State(Scope.Benchmark)
    public static class TestAddAndGetState {
        private AtomicInteger integer;

        @Setup(Level.Iteration)
        public void setup() {
            integer = new AtomicInteger();
        }

        public AtomicInteger getInteger() {
            return integer;
        }
    }
}

@Setup、@TearDown支援設定Level級別,Level有三個值:

  • Trial: 每次benchmark前/後執行一次,每次benchmark會包含多輪(Iteration)
  • Iteration: 每輪執行前/後執行一次
  • Invocation: 每次呼叫測試的方法前/後都執行一次,這個執行頻率會很高,一般用不上。

Fork

@Fork註解用來設定啟動的JVM程式數量,多個程式是序列的方式啟動的,多個程式可以減少偶發因素對測試結果的影響。

Thread

@Thread用來配置執行測試啟動的執行緒數量

Warmup

@Warmup 用來配置預熱的時間,如下所示配置預熱五輪,每輪1second,也就是說總共會預熱5s左右,在這5s內會不停的迴圈呼叫測試方法,但是預熱時的資料不作為測試結果參考。

@Warmup(iterations = 5, time = 1)

Measurement

@Measurement用來配置基準測試的時間,如下所示配置預熱10輪,每輪1second,也就是說總共會測試10s左右,在這10s內會不停的迴圈呼叫測試方法,同事測試資料會被基準測試結果參考。

@Measurement(iterations = 5, time = 1)

輸出測試結果

jmh支援多種格式的結果輸出text, csv, scsv, json, latex
如下列印出json格式的:

java -jar benchmark.jar -rf json

 

具體實踐可參考 HashMap 中7種遍歷方式的效能分析

 

 

參考

http://openjdk.java.net/projects/code-tools/jmh/

https://www.jianshu.com/p/2a83cc26d0e9

 

相關文章