<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JMH - Java 代碼性能測試的終極利器、必須掌握

          共 10122字,需瀏覽 21分鐘

           ·

          2020-08-23 05:20

          歡迎點(diǎn)擊?“未讀代碼” ,關(guān)注公眾號(hào),文章每周更新

          Java 性能測試難題

          現(xiàn)在的 JVM 已經(jīng)越來越為智能,它可以在編譯階段、加載階段、運(yùn)行階段對代碼進(jìn)行優(yōu)化。比如你寫了一段不怎么聰明的代碼,到了 JVM 這里,它發(fā)現(xiàn)幾處可以優(yōu)化的地方,就順手幫你優(yōu)化了一把。這對程序的運(yùn)行固然美妙,卻讓開發(fā)者不能準(zhǔn)確了解程序的運(yùn)行情況。在需要進(jìn)行性能測試時(shí),如果不知道 JVM 優(yōu)化細(xì)節(jié),可能會(huì)導(dǎo)致你的測試結(jié)果差之毫厘,失之千里,同樣的,Java 誕生之初就有一次編譯、隨處運(yùn)行的口號(hào),JVM 提供了底層支持,也提供了內(nèi)存管理機(jī)制,這些機(jī)制都會(huì)對我們的性能測試結(jié)果造成不可預(yù)測的影響。

          long start = System.currentTimeMillis();// ....long end = System.currentTimeMillis();System.out.println(end - start);

          上面可能就是你最常見的性能測試了,這樣的測試結(jié)果真的準(zhǔn)確嗎?答案是否定的,它有下面幾個(gè)問題。

          1. 時(shí)間精度問題,本身獲取到的時(shí)間戳就是存在誤差的,它和操作系統(tǒng)有關(guān)。
          2. JVM 在運(yùn)行時(shí)會(huì)進(jìn)行代碼預(yù)熱,說白了就是越跑越快。因?yàn)轭愋枰b載、需要準(zhǔn)備操作。
          3. JVM 會(huì)在各個(gè)階段都有可能對你的代碼進(jìn)行優(yōu)化處理。
          4. 資源回收的不確定性,可能運(yùn)行很快,回收很慢。

          帶著這些問題,突然發(fā)現(xiàn)進(jìn)行一次嚴(yán)格的基準(zhǔn)測試的難度大大增加。那么如何才能進(jìn)行一次嚴(yán)格的基準(zhǔn)測試呢?

          JMH 介紹

          那么如何對 Java 程序進(jìn)行一次精準(zhǔn)的性能測試呢?難道需要掌握很多 JVM 優(yōu)化細(xì)節(jié)嗎?難道要研究如何避免,并進(jìn)行正確編碼才能進(jìn)行嚴(yán)格的性能測試嗎?顯然不是,如果是這樣的話,未免過于困難了,好在有一款一款官方的微基準(zhǔn)測試工具 - JMH.

          JMH 的全名是 Java Microbenchmark Harness,它是由 Java 虛擬機(jī)團(tuán)隊(duì)開發(fā)的一款用于 Java 微基準(zhǔn)測試工具。用自己開發(fā)的工具測試自己開發(fā)的另一款工具,以子之矛,攻子之盾果真手到擒來,如臂使指。使用 JMH 可以讓你方便快速的進(jìn)行一次嚴(yán)格的代碼基準(zhǔn)測試,并且有多種測試模式,多種測試維度可供選擇;而且使用簡單、增加注解便可啟動(dòng)測試。

          JMH 使用

          JMH 的使用首先引入 maven 所需依賴,當(dāng)前最新版 為 1.23 版本。

              org.openjdk.jmh    jmh-core    1.23    org.openjdk.jmh    jmh-generator-annprocess    1.23    provided

          快速測試

          下面使用注解的方式指定測試參數(shù),通過一個(gè)例子展示 JMH 基準(zhǔn)測試的具體用法,先看一次運(yùn)行效果,然后再了解每個(gè)注解的具體含義。

          這個(gè)例子是使用 JMH 測試,使用加號(hào)拼接字符串和使用 StringBuilderappend 方法拼接字符串時(shí)的速度如何,每次拼接1000個(gè)數(shù)字進(jìn)行平均速度比較。

          import java.util.concurrent.TimeUnit;import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.RunnerException;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;
          /** *

          * JMH 基準(zhǔn)測試入門 * * @author niujinpeng * @Date 2020/8/21 1:13 */@BenchmarkMode(Mode.AverageTime)@State(Scope.Thread)@Fork(1)@OutputTimeUnit(TimeUnit.MILLISECONDS)@Warmup(iterations = 3)@Measurement(iterations = 5)public class JmhHello {
          String string = ""; StringBuilder stringBuilder = new StringBuilder();
          @Benchmark public String stringAdd() { for (int i = 0; i < 1000; i++) { string = string + i; } return string; }
          @Benchmark public String stringBuilderAppend() { for (int i = 0; i < 1000; i++) { stringBuilder.append(i); } return stringBuilder.toString(); }
          public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JmhHello.class.getSimpleName()) .build(); new Runner(opt).run(); }}

          代碼很簡單,不做解釋,stringAdd 使用加號(hào)拼接字符串 1000次,stringBuilderAppend 使用 append 拼接字符串 1000次。直接運(yùn)行 main 方法,稍等片刻后可以得到詳細(xì)的運(yùn)行輸出結(jié)果。

          // 開始測試 stringAdd 方法# JMH version: 1.23# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13# VM invoker: D:\develop\Java\jdk8_181\jre\bin\java.exe# VM options: -javaagent:C:\ideaIU-2020.1.3.win\lib\idea_rt.jar=50363:C:\ideaIU-2020.1.3.win\bin -Dfile.encoding=UTF-8# Warmup: 3 iterations, 10 s each  // 預(yù)熱運(yùn)行三次# Measurement: 5 iterations, 10 s each // 性能測試5次 # Timeout: 10 min per iteration  // 超時(shí)時(shí)間10分鐘# Threads: 1 thread, will synchronize iterations  // 線程數(shù)量為1# Benchmark mode: Average time, time/op  // 統(tǒng)計(jì)方法調(diào)用一次的平均時(shí)間# Benchmark: net.codingme.jmh.JmhHello.stringAdd // 本次執(zhí)行的方法
          # Run progress: 0.00% complete, ETA 00:02:40# Fork: 1 of 1# Warmup Iteration 1: 95.153 ms/op // 第一次預(yù)熱,耗時(shí)95ms# Warmup Iteration 2: 108.927 ms/op // 第二次預(yù)熱,耗時(shí)108ms# Warmup Iteration 3: 167.760 ms/op // 第三次預(yù)熱,耗時(shí)167msIteration 1: 198.897 ms/op // 執(zhí)行五次耗時(shí)度量Iteration 2: 243.437 ms/opIteration 3: 271.171 ms/opIteration 4: 295.636 ms/opIteration 5: 327.822 ms/op

          Result "net.codingme.jmh.JmhHello.stringAdd": 267.393 ±(99.9%) 189.907 ms/op [Average] (min, avg, max) = (198.897, 267.393, 327.822), stdev = 49.318 // 執(zhí)行的最小、平均、最大、誤差值 CI (99.9%): [77.486, 457.299] (assumes normal distribution) // 開始測試 stringBuilderAppend 方法# Benchmark: net.codingme.jmh.JmhHello.stringBuilderAppend
          # Run progress: 50.00% complete, ETA 00:01:21# Fork: 1 of 1# Warmup Iteration 1: 1.872 ms/op# Warmup Iteration 2: 4.491 ms/op# Warmup Iteration 3: 5.866 ms/opIteration 1: 6.936 ms/opIteration 2: 8.465 ms/opIteration 3: 8.925 ms/opIteration 4: 9.766 ms/opIteration 5: 10.143 ms/op

          Result "net.codingme.jmh.JmhHello.stringBuilderAppend": 8.847 ±(99.9%) 4.844 ms/op [Average] (min, avg, max) = (6.936, 8.847, 10.143), stdev = 1.258 CI (99.9%): [4.003, 13.691] (assumes normal distribution)

          # Run complete. Total time: 00:02:42
          REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up onwhy the numbers are the way they are. Use profilers (see -prof, -lprof), design factorialexperiments, perform baseline and negative tests that provide experimental control, make surethe 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.// 測試結(jié)果對比Benchmark Mode Cnt Score Error UnitsJmhHello.stringAdd avgt 5 267.393 ± 189.907 ms/opJmhHello.stringBuilderAppend avgt 5 8.847 ± 4.844 ms/op
          Process finished with exit code 0

          上面日志里的 // 注釋是我手動(dòng)增加上去的,其實(shí)我們只需要看下面的最終結(jié)果就可以了,可以看到 stringAdd 方法平均耗時(shí) 267.393ms,而 ?stringBuilderAppend 方法平均耗時(shí)只有 8.847ms,可見 StringBuilderappend 方法進(jìn)行字符串拼接速度快的多,這也是我們推薦使用append 進(jìn)行字符串拼接的原因。

          注解說明

          經(jīng)過上面的示例,想必你也可以快速的使用 JMH 進(jìn)行基準(zhǔn)測試了,不過上面的諸多注解你可能還有疑惑,下面一一介紹。

          類上使用了六個(gè)注解。

          @BenchmarkMode(Mode.AverageTime)@State(Scope.Thread)@Fork(1)@OutputTimeUnit(TimeUnit.MILLISECONDS)@Warmup(iterations = 3)@Measurement(iterations = 5)

          @BenchmarkMode(Mode.AverageTime) 表示統(tǒng)計(jì)平均響應(yīng)時(shí)間,不僅可以用在類上,也可用在測試方法上。

          除此之外還可以取值:

          • Throughput:統(tǒng)計(jì)單位時(shí)間內(nèi)可以對方法測試多少次。
          • SampleTime:統(tǒng)計(jì)每個(gè)響應(yīng)時(shí)間范圍內(nèi)的響應(yīng)次數(shù),比如 0-1ms,3次;1-2ms,5次。
          • SingleShotTime:跳過預(yù)熱階段,直接進(jìn)行一次****微基準(zhǔn)測試。

          @State(Scope.Thread):每個(gè)進(jìn)行基準(zhǔn)測試的線程都會(huì)獨(dú)享一個(gè)對象示例。

          除此之外還能取值:

          • Benchmark:多線程共享一個(gè)示例。
          • Group:線程組共享一個(gè)示例,在測試方法上使用 @Group 設(shè)置線程組。

          @Fork(1):表示開啟一個(gè)線程進(jìn)行測試。

          **OutputTimeUnit(TimeUnit.MILLISECONDS):輸出的時(shí)間單位,這里寫的是毫秒。

          @Warmup(iterations = 3):微基準(zhǔn)測試前進(jìn)行三次預(yù)熱執(zhí)行,也可用在測試方法上。

          @Measurement(iterations = 5):進(jìn)行 5 次微基準(zhǔn)測試,也可用在測試方法上。

          在兩個(gè)測試方法上只使用了一個(gè)注解 @Benchmark,這個(gè)注解表示這個(gè)方法是要進(jìn)行基準(zhǔn)測試的方法,它類似于 Junit 中的 @Test 注解。上面還提到某些注解還可以用到測試方法上,也就是使用了 @Benchmark 的方法之上,如果類上和測試方法同時(shí)存在注解,會(huì)以方法上的注解為準(zhǔn)。

          其實(shí) JMH 也可以把這些參數(shù)直接在 main 方法中指定,這時(shí) main 方法中指定的級(jí)別最高。

          public static void main(String[] args) throws RunnerException {    Options opt = new OptionsBuilder()            .include(JmhHello.class.getSimpleName())            .forks(1)            .warmupIterations(5)            .measurementIterations(10)            .build();    new Runner(opt).run();}

          正確的微基準(zhǔn)測試

          如果編寫的代碼本身就存在著諸多問題,那么即使使用正確的測試方法,也不可能得到正確的測試結(jié)果。這些測試代碼中的問題應(yīng)該由我們進(jìn)行主動(dòng)避免,那么有哪些常見問題呢?下面介紹兩種最常見的情況。

          無用代碼消除 ( Dead Code Elimination )

          也有網(wǎng)友形象的翻譯成死代碼,死代碼是指那些 JVM 經(jīng)過檢查發(fā)現(xiàn)的根本不會(huì)使用到的代碼。比如下面這個(gè)代碼片段。

          import java.util.concurrent.TimeUnit;import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.RunnerException;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;
          /** *

          * 測試死代碼消除 * * @author niujinpeng * @Date 2020/8/21 8:04 */@BenchmarkMode(Mode.AverageTime)@State(Scope.Thread)@Fork(1)@OutputTimeUnit(TimeUnit.MICROSECONDS)@Warmup(iterations = 3, time = 3)@Measurement(iterations = 5, time = 3)public class JmhDCE {
          @Benchmark public double test1() { return Math.log(Math.PI); } @Benchmark public void test2() { double result = Math.log(Math.PI); result = Math.log(result); }
          public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JmhDCE.class.getSimpleName()) .build(); new Runner(opt).run(); }}

          在這個(gè)代碼片段里里,test1 方法對圓周率進(jìn)行對數(shù)計(jì)算,并返回計(jì)算結(jié)果;而 test2 中不僅對圓周率進(jìn)行對數(shù)計(jì)算,還對計(jì)算的結(jié)果再次對數(shù)計(jì)算,看起來復(fù)雜一些,但是因?yàn)闆]有用到計(jì)算結(jié)果,所以 JVM 會(huì)自動(dòng)消除這段代碼, 因?yàn)樗鼪]有任何意義。

          Benchmark     Mode  Cnt   Score    Error  UnitsJmhDCE.test1  avgt    5   0.002 ±  0.001  us/opJmhDCE.test2  avgt    5  ≈ 10??           us/op

          測試結(jié)果里也可以看到 test 平均耗時(shí) 0.0004 微秒,而 test1 平均耗時(shí) 0.002 微秒。

          常量折疊 (Constant Folding)

          在對 Java 源文件編譯的過程中,編譯器通過語法分析,可以發(fā)現(xiàn)某些能直接得到計(jì)算結(jié)果而不會(huì)再次更改的代碼,然后會(huì)將計(jì)算結(jié)果記錄下來,這樣在執(zhí)行的過程中就不需要再次運(yùn)算了。比如這段代碼。

          import java.util.concurrent.TimeUnit;import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.RunnerException;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;
          /** *

          * 測試常量折疊 * * @author niujinpeng * @Date 2020/8/21 8:23 */@BenchmarkMode(Mode.AverageTime)@State(Scope.Thread)@Fork(1)@OutputTimeUnit(TimeUnit.MICROSECONDS)@Warmup(iterations = 3, time = 3)@Measurement(iterations = 5, time = 3)public class JmhConstantFolding {
          final double PI1 = 3.14159265358979323846; double PI2 = 3.14159265358979323846;
          @Benchmark public double test1() { return Math.log(PI1) * Math.log(PI1); }
          @Benchmark public double test2() { return Math.log(PI2) * Math.log(PI2); }
          public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(JmhConstantFolding.class.getSimpleName()).build(); new Runner(opt).run(); }}

          test1 中使用 final 修飾的 PI1 進(jìn)行對象計(jì)算,因?yàn)?PI1 不能再次更改,所以 test1 的計(jì)算結(jié)果必定是不會(huì)更改的,所以 JVM 會(huì)進(jìn)行常量折疊優(yōu)化,而 test2 使用的 PI2 可能會(huì)被修改,所以只能每次進(jìn)行計(jì)算。

          Benchmark                 Mode  Cnt  Score    Error  UnitsJmhConstantFolding.test1  avgt    5  0.002 ±  0.001  us/opJmhConstantFolding.test2  avgt    5  0.019 ±  0.001  us/op

          可以看到 test2 耗時(shí)要多的多,達(dá)到了 0.019 微秒。

          其實(shí) JVM 做的優(yōu)化操作遠(yuǎn)不止上面這些,還有比如常量傳播(Constant Propagation)、循環(huán)展開(Loop Unwinding)、循環(huán)表達(dá)式外提(Loop Expression Hoisting)、消除公共子表達(dá)式(Common Subexpression Elimination)、本塊重排序(Basic Block Reordering)、范圍檢查消除(Range Check Elimination)等。

          總結(jié)

          JMH 進(jìn)行基準(zhǔn)測試的使用過程并不復(fù)雜,同為 Java 虛擬機(jī)團(tuán)隊(duì)開發(fā),準(zhǔn)確性毋容置疑。但是在進(jìn)行基準(zhǔn)測試時(shí)還是要注意自己的代碼問題,如果編寫的要進(jìn)行測試的代碼本身存在問題,那么測試的結(jié)果必定是不準(zhǔn)的。掌握了 JMH 基準(zhǔn)測試之后,可以嘗試測試一些常用的工具或者框架的性能如何,看看哪個(gè)工具的性能最好,比如 FastJSON 真的比 GSON 在進(jìn)行 JSON 轉(zhuǎn)換時(shí)更 Fast 嗎?Spring 的 BeanUtils 和 Apache 的 BeanUtils 哪個(gè)速度更快?

          參考:

          • https://www.ibm.com/developerworks/cn/java/j-benchmark1.html

          • http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

          • 深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第3版)第11章 后端編譯與優(yōu)化

          最后的話

          文章有幫助可以點(diǎn)個(gè)「在看」或「分享」,都是支持,我都喜歡!
          文章每周持續(xù)更新,要實(shí)時(shí)關(guān)注我更新的文章以及分享的干貨,可以關(guān)注「?未讀代碼?」公眾號(hào)。

          ---- END ----

          "未讀代碼,一線技術(shù)工具人的學(xué)習(xí)、生活與見聞"

          一個(gè)「在看」,一段時(shí)光?

          瀏覽 70
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操逼动漫视频 | 国产熟妇疯狂性做爰XXXⅩ网站 | 伊人大香蕉在线影院 | 天天日天天综合 | 中国黄色在线视频 |