性能調(diào)優(yōu)必備利器之 JMH

if 快還是 switch 快究履?HashMap 的初始化 size 要不要指定循未,指定之后性能可以提高多少涂圆?各種序列化方法哪個(gè)耗時(shí)更短泼返?

無論出自何種原因需要進(jìn)行性能評(píng)估硝逢,量化指標(biāo)總是必要的。

在大部分場(chǎng)合绅喉,簡(jiǎn)單地回答誰快誰慢是遠(yuǎn)遠(yuǎn)不夠的渠鸽,如何將程序性能量化呢?

這就需要我們的主角 JMH 登場(chǎng)了柴罐!

JMH 簡(jiǎn)介

JMH(Java Microbenchmark Harness)是用于代碼微基準(zhǔn)測(cè)試的工具套件徽缚,主要是基于方法層面的基準(zhǔn)測(cè)試,精度可以達(dá)到納秒級(jí)丽蝎。該工具是由 Oracle 內(nèi)部實(shí)現(xiàn) JIT 的大牛們編寫的猎拨,他們應(yīng)該比任何人都了解 JIT 以及 JVM 對(duì)于基準(zhǔn)測(cè)試的影響。

當(dāng)你定位到熱點(diǎn)方法屠阻,希望進(jìn)一步優(yōu)化方法性能的時(shí)候,就可以使用 JMH 對(duì)優(yōu)化的結(jié)果進(jìn)行量化的分析额各。

JMH 比較典型的應(yīng)用場(chǎng)景如下:

  1. 想準(zhǔn)確地知道某個(gè)方法需要執(zhí)行多長時(shí)間国觉,以及執(zhí)行時(shí)間和輸入之間的相關(guān)性
  2. 對(duì)比接口不同實(shí)現(xiàn)在給定條件下的吞吐量
  3. 查看多少百分比的請(qǐng)求在多長時(shí)間內(nèi)完成

下面我們以字符串拼接的兩種方法為例子使用 JMH 做基準(zhǔn)測(cè)試。

加入依賴

因?yàn)?JMH 是 JDK9 自帶的虾啦,如果是 JDK9 之前的版本需要加入如下依賴(目前 JMH 的最新版本為 1.23):

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

編寫基準(zhǔn)測(cè)試

接下來麻诀,創(chuàng)建一個(gè) JMH 測(cè)試類,用來判斷 +StringBuilder.append() 兩種字符串拼接哪個(gè)耗時(shí)更短傲醉,具體代碼如下所示:

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringConnectTest {

    @Param(value = {"10", "50", "100"})
    private int length;

    @Benchmark
    public void testStringAdd(Blackhole blackhole) {
        String a = "";
        for (int i = 0; i < length; i++) {
            a += i;
        }
        blackhole.consume(a);
    }

    @Benchmark
    public void testStringBuilderAdd(Blackhole blackhole) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            sb.append(i);
        }
        blackhole.consume(sb.toString());
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(StringConnectTest.class.getSimpleName())
                .result("result.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }
}

其中需要測(cè)試的方法用 @Benchmark 注解標(biāo)識(shí)蝇闭,這些注解的具體含義將在下面介紹。

在 main() 函數(shù)中硬毕,首先對(duì)測(cè)試用例進(jìn)行配置呻引,使用 Builder 模式配置測(cè)試,將配置參數(shù)存入 Options 對(duì)象吐咳,并使用 Options 對(duì)象構(gòu)造 Runner 啟動(dòng)測(cè)試逻悠。

另外大家可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

執(zhí)行基準(zhǔn)測(cè)試

準(zhǔn)備工作做好了元践,接下來,運(yùn)行代碼童谒,等待片刻单旁,測(cè)試結(jié)果就出來了,下面對(duì)結(jié)果做下簡(jiǎn)單說明:

# JMH version: 1.23
# VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09
# VM invoker: D:\Software\Java\jdk1.8.0_201\jre\bin\java.exe
# VM options: -javaagent:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=61018:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.wupx.jmh.StringConnectTest.testStringBuilderAdd
# Parameters: (length = 100)

該部分為測(cè)試的基本信息饥伊,比如使用的 Java 路徑象浑,預(yù)熱代碼的迭代次數(shù),測(cè)量代碼的迭代次數(shù)琅豆,使用的線程數(shù)量融柬,測(cè)試的統(tǒng)計(jì)單位等。

# Warmup Iteration   1: 1083.569 ±(99.9%) 393.884 ns/op
# Warmup Iteration   2: 864.685 ±(99.9%) 174.120 ns/op
# Warmup Iteration   3: 798.310 ±(99.9%) 121.161 ns/op

該部分為每一次熱身中的性能指標(biāo)趋距,預(yù)熱測(cè)試不會(huì)作為最終的統(tǒng)計(jì)結(jié)果粒氧。預(yù)熱的目的是讓 JVM 對(duì)被測(cè)代碼進(jìn)行足夠多的優(yōu)化,比如节腐,在預(yù)熱后外盯,被測(cè)代碼應(yīng)該得到了充分的 JIT 編譯和優(yōu)化。

Iteration   1: 810.667 ±(99.9%) 51.505 ns/op
Iteration   2: 807.861 ±(99.9%) 13.163 ns/op
Iteration   3: 851.421 ±(99.9%) 33.564 ns/op
Iteration   4: 805.675 ±(99.9%) 33.038 ns/op
Iteration   5: 821.020 ±(99.9%) 66.943 ns/op

Result "com.wupx.jmh.StringConnectTest.testStringBuilderAdd":
  819.329 ±(99.9%) 72.698 ns/op [Average]
  (min, avg, max) = (805.675, 819.329, 851.421), stdev = 18.879
  CI (99.9%): [746.631, 892.027] (assumes normal distribution)

Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op

該部分顯示測(cè)量迭代的情況翼雀,每一次迭代都顯示了當(dāng)前的執(zhí)行速率饱苟,即一個(gè)操作所花費(fèi)的時(shí)間。在進(jìn)行 5 次迭代后狼渊,進(jìn)行統(tǒng)計(jì)箱熬,在本例中,length 為 100 的情況下 testStringBuilderAdd 方法的平均執(zhí)行花費(fèi)時(shí)間為 819.329 ns狈邑,誤差為 72.698 ns城须。

最后的測(cè)試結(jié)果如下所示:

Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringAdd               10  avgt    5   161.496 ±  17.097  ns/op
StringConnectTest.testStringAdd               50  avgt    5  1854.657 ± 227.902  ns/op
StringConnectTest.testStringAdd              100  avgt    5  6490.062 ± 327.626  ns/op
StringConnectTest.testStringBuilderAdd        10  avgt    5    68.769 ±   4.460  ns/op
StringConnectTest.testStringBuilderAdd        50  avgt    5   413.021 ±  30.950  ns/op
StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op

結(jié)果表明,在拼接字符次數(shù)越多的情況下米苹,StringBuilder.append() 的性能就更好糕伐。

生成 jar 包執(zhí)行

對(duì)于一些小測(cè)試,直接用上面的方式寫一個(gè) main 函數(shù)手動(dòng)執(zhí)行就好了蘸嘶。

對(duì)于大型的測(cè)試良瞧,需要測(cè)試的時(shí)間比較久、線程數(shù)比較多训唱,加上測(cè)試的服務(wù)器需要褥蚯,一般要放在 Linux 服務(wù)器里去執(zhí)行。

JMH 官方提供了生成 jar 包的方式來執(zhí)行况增,我們需要在 maven 里增加一個(gè) plugin赞庶,具體配置如下:

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.4.1</version>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
                <configuration>
                    <finalName>jmh-demo</finalName>
                    <transformers>
                        <transformer
                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <mainClass>org.openjdk.jmh.Main</mainClass>
                        </transformer>
                    </transformers>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

接著執(zhí)行 maven 的命令生成可執(zhí)行 jar 包并執(zhí)行:

mvn clean install
java -jar target/jmh-demo.jar StringConnectTest

JMH 基礎(chǔ)

為了能夠更好地使用 JMH 的各項(xiàng)功能,下面對(duì) JMH 的基本概念進(jìn)行講解:

@BenchmarkMode

用來配置 Mode 選項(xiàng),可用于類或者方法上尘执,這個(gè)注解的 value 是一個(gè)數(shù)組舍哄,可以把幾種 Mode 集合在一起執(zhí)行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime})誊锭,還可以設(shè)置為 Mode.All表悬,即全部執(zhí)行一遍。

  1. Throughput:整體吞吐量丧靡,每秒執(zhí)行了多少次調(diào)用蟆沫,單位為 ops/time
  2. AverageTime:用的平均時(shí)間,每次操作的平均時(shí)間温治,單位為 time/op
  3. SampleTime:隨機(jī)取樣饭庞,最后輸出取樣結(jié)果的分布
  4. SingleShotTime:只運(yùn)行一次,往往同時(shí)把 Warmup 次數(shù)設(shè)為 0熬荆,用于測(cè)試?yán)鋯?dòng)時(shí)的性能
  5. All:上面的所有模式都執(zhí)行一次

@State

通過 State 可以指定一個(gè)對(duì)象的作用范圍舟山,JMH 根據(jù) scope 來進(jìn)行實(shí)例化和共享操作。@State 可以被繼承使用卤恳,如果父類定義了該注解累盗,子類則無需定義。由于 JMH 允許多線程同時(shí)執(zhí)行測(cè)試突琳,不同的選項(xiàng)含義如下:

  1. Scope.Benchmark:所有測(cè)試線程共享一個(gè)實(shí)例若债,測(cè)試有狀態(tài)實(shí)例在多線程共享下的性能
  2. Scope.Group:同一個(gè)線程在同一個(gè) group 里共享實(shí)例
  3. Scope.Thread:默認(rèn)的 State,每個(gè)測(cè)試線程分配一個(gè)實(shí)例

@OutputTimeUnit

為統(tǒng)計(jì)結(jié)果的時(shí)間單位拆融,可用于類或者方法注解

@Warmup

預(yù)熱所需要配置的一些基本測(cè)試參數(shù)蠢琳,可用于類或者方法上。一般前幾次進(jìn)行程序測(cè)試的時(shí)候都會(huì)比較慢镜豹,所以要讓程序進(jìn)行幾輪預(yù)熱傲须,保證測(cè)試的準(zhǔn)確性。參數(shù)如下所示:

  1. iterations:預(yù)熱的次數(shù)
  2. time:每次預(yù)熱的時(shí)間
  3. timeUnit:時(shí)間的單位逛艰,默認(rèn)秒
  4. batchSize:批處理大小躏碳,每次操作調(diào)用幾次方法

為什么需要預(yù)熱?

因?yàn)?JVM 的 JIT 機(jī)制的存在散怖,如果某個(gè)函數(shù)被調(diào)用多次之后,JVM 會(huì)嘗試將其編譯為機(jī)器碼肄渗,從而提高執(zhí)行速度镇眷,所以為了讓 benchmark 的結(jié)果更加接近真實(shí)情況就需要進(jìn)行預(yù)熱。

@Measurement

實(shí)際調(diào)用方法所需要配置的一些基本測(cè)試參數(shù)翎嫡,可用于類或者方法上欠动,參數(shù)和 @Warmup 相同。

@Threads

每個(gè)進(jìn)程中的測(cè)試線程,可用于類或者方法上具伍。

@Fork

進(jìn)行 fork 的次數(shù)翅雏,可用于類或者方法上。如果 fork 數(shù)是 2 的話人芽,則 JMH 會(huì) fork 出兩個(gè)進(jìn)程來進(jìn)行測(cè)試望几。

@Param

指定某項(xiàng)參數(shù)的多種情況,特別適合用來測(cè)試一個(gè)函數(shù)在不同的參數(shù)輸入的情況下的性能萤厅,只能作用在字段上橄抹,使用該注解必須定義 @State 注解。

在介紹完常用的注解后惕味,讓我們來看下 JMH 有哪些陷阱楼誓。

JMH 陷阱

在使用 JMH 的過程中,一定要避免一些陷阱名挥。

比如 JIT 優(yōu)化中的死碼消除疟羹,比如以下代碼:

@Benchmark
public void testStringAdd(Blackhole blackhole) {
    String a = "";
    for (int i = 0; i < length; i++) {
        a += i;
    }
}

JVM 可能會(huì)認(rèn)為變量 a 從來沒有使用過,從而進(jìn)行優(yōu)化把整個(gè)方法內(nèi)部代碼移除掉禀倔,這就會(huì)影響測(cè)試結(jié)果榄融。

JMH 提供了兩種方式避免這種問題,一種是將這個(gè)變量作為方法返回值 return a蹋艺,一種是通過 Blackhole 的 consume 來避免 JIT 的優(yōu)化消除剃袍。

其他陷阱還有常量折疊與常量傳播、永遠(yuǎn)不要在測(cè)試中寫循環(huán)捎谨、使用 Fork 隔離多個(gè)測(cè)試方法民效、方法內(nèi)聯(lián)、偽共享與緩存行涛救、分支預(yù)測(cè)畏邢、多線程測(cè)試等,感興趣的可以閱讀 https://github.com/lexburner/JMH-samples 了解全部的陷阱检吆。

JMH 插件

大家還可以通過 IDEA 安裝 JMH 插件使 JMH 更容易實(shí)現(xiàn)基準(zhǔn)測(cè)試舒萎,在 IDEA 中點(diǎn)擊 File->Settings...->Plugins,然后搜索 jmh蹭沛,選擇安裝 JMH plugin:

這個(gè)插件可以讓我們能夠以 JUnit 相同的方式使用 JMH臂寝,主要功能如下:

  1. 自動(dòng)生成帶有 @Benchmark 的方法
  2. 像 JUnit 一樣,運(yùn)行單獨(dú)的 Benchmark 方法
  3. 運(yùn)行類中所有的 Benchmark 方法

比如可以通過右鍵點(diǎn)擊 Generate...摊灭,選擇操作 Generate JMH benchmark 就可以生成一個(gè)帶有 @Benchmark 的方法咆贬。

還有將光標(biāo)移動(dòng)到方法聲明并調(diào)用 Run 操作就運(yùn)行一個(gè)單獨(dú)的 Benchmark 方法。

將光標(biāo)移到類名所在行帚呼,右鍵點(diǎn)擊 Run 運(yùn)行掏缎,該類下的所有被 @Benchmark 注解的方法都會(huì)被執(zhí)行皱蹦。

JMH 可視化

除此以外,如果你想將測(cè)試結(jié)果以圖表的形式可視化眷蜈,可以試下這些網(wǎng)站:

比如將上面測(cè)試?yán)咏Y(jié)果的 json 文件導(dǎo)入沪哺,就可以實(shí)現(xiàn)可視化:

總結(jié)

本文主要介紹了性能基準(zhǔn)測(cè)試工具 JMH,它可以通過一些功能來規(guī)避由 JVM 中的 JIT 或者其他優(yōu)化對(duì)性能測(cè)試造成的影響酌儒。

只需要將待測(cè)的業(yè)務(wù)邏輯用 @Benchmark 注解標(biāo)識(shí)辜妓,就可以讓 JMH 的注解處理器自動(dòng)生成真正的性能測(cè)試代碼,以及相應(yīng)的性能測(cè)試配置文件今豆。

最好的關(guān)系就是互相成就嫌拣,大家的在看、轉(zhuǎn)發(fā)呆躲、留言三連就是我創(chuàng)作的最大動(dòng)力异逐。

參考

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

深入拆解Java虛擬機(jī)

《實(shí)戰(zhàn)Java高并發(fā)程序設(shè)計(jì)》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市插掂,隨后出現(xiàn)的幾起案子灰瞻,更是在濱河造成了極大的恐慌,老刑警劉巖辅甥,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酝润,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡璃弄,警方通過查閱死者的電腦和手機(jī)要销,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夏块,“玉大人疏咐,你說我怎么就攤上這事∑旯” “怎么了浑塞?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長政己。 經(jīng)常有香客問我酌壕,道長,這世上最難降的妖魔是什么歇由? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任卵牍,我火速辦了婚禮,結(jié)果婚禮上沦泌,老公的妹妹穿的比我還像新娘辽慕。我一直安慰自己,他們只是感情好赦肃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般他宛。 火紅的嫁衣襯著肌膚如雪船侧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天厅各,我揣著相機(jī)與錄音镜撩,去河邊找鬼。 笑死队塘,一個(gè)胖子當(dāng)著我的面吹牛袁梗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播憔古,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼遮怜,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了鸿市?” 一聲冷哼從身側(cè)響起锯梁,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎焰情,沒想到半個(gè)月后陌凳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡内舟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年合敦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片验游。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡充岛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出批狱,到底是詐尸還是另有隱情裸准,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布赔硫,位于F島的核電站炒俱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏爪膊。R本人自食惡果不足惜权悟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望推盛。 院中可真熱鬧峦阁,春花似錦、人聲如沸耘成。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至撒会,卻和暖如春嘹朗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诵肛。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來泰國打工屹培, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怔檩。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓褪秀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親薛训。 傳聞我的和親對(duì)象是個(gè)殘疾皇子媒吗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容