java jmh 使用

代码工具:jmh JMH是一个Java工具,用于构建、运行和分析nano/micro/mili/macro基准,这些基准是用Java和其他针对JVM的语言编写的。
Code Tools: jmh JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM.

JMH解决了什么问题

JMH怎么用

  1. 添加对应jar包
  2. 根据实际情况配置参数
  3. 编写代码运行

添加依赖

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

配置参数

@BenchmarkMode
用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。

Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
SampleTime:随机取样,最后输出取样结果的分布
SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
All:上面的所有模式都执行一次
@State
通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:

Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
Scope.Group:同一个线程在同一个 group 里共享实例
Scope.Thread:默认的 State,每个测试线程分配一个实例
@OutputTimeUnit
为统计结果的时间单位,可用于类或者方法注解

@Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:

iterations:预热的次数
time:每次预热的时间
timeUnit:时间的单位,默认秒
batchSize:批处理大小,每次操作调用几次方法
为什么需要预热?

因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@Measurement
实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup 相同。

@Threads
每个进程中的测试线程,可用于类或者方法上。

@Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。

@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。

编写代码运行

建议 代码里不要打日志

测试1

import cn.wkq.java.string.StringInternTest;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.results.format.ResultFormatType;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <pre>
 *
 *     JMH使用样例
 *         http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
 *
 * </pre>
 *
 * @author weikeqin
 * @date 2020-06-14 17:11
 */
public class JmtTest {

    private static final transient Logger log = LoggerFactory.getLogger(StringInternTest.class);

    @Benchmark
    public void print() {
        if (log.isDebugEnabled()) {
            log.debug("hello JMH");
        }
    }


    /**
     * @param args
     */
    public static void main(String[] args) throws RunnerException {

        log.info("开始执行。");

        Options opt = new OptionsBuilder()
                .include(JmtTest.class.getSimpleName())
                .forks(1)
                .result("jmh_result.json")
                .resultFormat(ResultFormatType.JSON)
                .build();

        new Runner(opt).run();

        log.info("执行完成。");
    }

}
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.openjdk.jmh.util.Utils (file:/Users/weikeqin1/.m2/repository/org/openjdk/jmh/jmh-core/1.23/jmh-core-1.23.jar) to field java.io.PrintStream.charOut
WARNING: Please consider reporting this to the maintainers of org.openjdk.jmh.util.Utils
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
# JMH version: 1.23
# VM version: JDK 14.0.1, Java HotSpot(TM) 64-Bit Server VM, 14.0.1+7
# VM invoker: /Library/Java/JavaVirtualMachines/jdk-14.0.1.jdk/Contents/Home/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=57492:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# 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: cn.wkq.java.jmh.JmtTest.print

# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration   1: 660097134.157 ops/s
# Warmup Iteration   2: 650765740.996 ops/s
# Warmup Iteration   3: 661961002.718 ops/s
# Warmup Iteration   4: 672329475.200 ops/s
# Warmup Iteration   5: 611127628.164 ops/s
Iteration   1: 615120987.535 ops/s
Iteration   2: 645989044.177 ops/s
Iteration   3: 642876939.314 ops/s
Iteration   4: 640265656.368 ops/s
Iteration   5: 673185386.638 ops/s


Result "cn.wkq.java.jmh.JmtTest.print":
  643487602.806 ±(99.9%) 79467866.034 ops/s [Average]
  (min, avg, max) = (615120987.535, 643487602.806, 673185386.638), stdev = 20637552.448
  CI (99.9%): [564019736.772, 722955468.841] (assumes normal distribution)


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

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
JmtTest.print  thrpt    5  643487602.806 ± 79467866.034  ops/s

Benchmark result is saved to jmh_result.json
[
    {
        "jmhVersion" : "1.23",
        "benchmark" : "cn.wkq.java.jmh.JmtTest.print",
        "mode" : "thrpt",
        "threads" : 1,
        "forks" : 1,
        "jvm" : "/Library/Java/JavaVirtualMachines/jdk-14.0.1.jdk/Contents/Home/bin/java",
        "jvmArgs" : [
            "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=57492:/Applications/IntelliJ IDEA.app/Contents/bin",
            "-Dfile.encoding=UTF-8"
        ],
        "jdkVersion" : "14.0.1",
        "vmName" : "Java HotSpot(TM) 64-Bit Server VM",
        "vmVersion" : "14.0.1+7",
        "warmupIterations" : 5,
        "warmupTime" : "10 s",
        "warmupBatchSize" : 1,
        "measurementIterations" : 5,
        "measurementTime" : "10 s",
        "measurementBatchSize" : 1,
        "primaryMetric" : {
            "score" : 6.434876028063821E8,
            "scoreError" : 7.946786603420861E7,
            "scoreConfidence" : [
                5.640197367721734E8,
                7.229554688405907E8
            ],
            "scorePercentiles" : {
                "0.0" : 6.151209875347286E8,
                "50.0" : 6.428769393141737E8,
                "90.0" : 6.731853866384177E8,
                "95.0" : 6.731853866384177E8,
                "99.0" : 6.731853866384177E8,
                "99.9" : 6.731853866384177E8,
                "99.99" : 6.731853866384177E8,
                "99.999" : 6.731853866384177E8,
                "99.9999" : 6.731853866384177E8,
                "100.0" : 6.731853866384177E8
            },
            "scoreUnit" : "ops/s",
            "rawData" : [
                [
                    6.151209875347286E8,
                    6.459890441766258E8,
                    6.428769393141737E8,
                    6.402656563679644E8,
                    6.731853866384177E8
                ]
            ]
        },
        "secondaryMetrics" : {
        }
    }
]

测试2

import org.junit.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.results.format.ResultFormatType;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * <pre>
 *
 *     JMH使用样例
 *         http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
 *
 * </pre>
 *
 * @author weikeqin
 * @date 2020-06-14 16:06
 */
@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 StringInternTest {

    private static final transient Logger log = LoggerFactory.getLogger(StringInternTest.class);

    @Param(value = {"10000", "100000", "1000000"})
    private int count;

    /**
     *
     */
    @Benchmark
    public void stringTestWithIntern() {

        long fm1 = Runtime.getRuntime().freeMemory();

        List<SharedLocation> list = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            String country = "中国";
            String province = "山西";
            String city = "运城";
            SharedLocation sharedLocation = new SharedLocation();
            sharedLocation.setCountry(country.intern());
            sharedLocation.setProvince(province.intern());
            sharedLocation.setCity(city.intern());

            list.add(sharedLocation);


        }

        log.info("个数:{}", list.size());

        long fm2 = Runtime.getRuntime().freeMemory();

        log.info("{}  {}    use {} bytes", fm1, fm2, fm1 - fm2);
    }

    /**
     *
     */
    @Benchmark
    public void stringTestWithOutIntern() {

        long fm1 = Runtime.getRuntime().freeMemory();

        List<SharedLocation> list = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            String country = "中国";
            String province = "山西";
            String city = "运城";
            SharedLocation sharedLocation = new SharedLocation();
            sharedLocation.setCountry(country);
            sharedLocation.setProvince(province);
            sharedLocation.setCity(city);

            list.add(sharedLocation);

        }

        log.info("个数:{}", list.size());

        long fm2 = Runtime.getRuntime().freeMemory();

        log.info("{}  {}    use {} bytes", fm1, fm2, fm1 - fm2);

    }


    /**
     * @param args
     */
    public static void main(String[] args) {

        log.info("开始执行。");

        Options opt = new OptionsBuilder()
                .include(StringInternTest.class.getSimpleName())
                .result("result.json")
                .resultFormat(ResultFormatType.JSON).build();

        try {
            new Runner(opt).run();
        } catch (RunnerException e) {
            log.error("", e);
        }

        log.info("执行完成。");
    }

}


/**
 *
 */
class SharedLocation {

    /**
     * 市
     */
    private String city;
    /**
     * 省
     */
    private String province;
    /**
     * 国家
     */
    private String country;

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getProvince() {
        return province;
    }

    public void setProvince(String province) {
        this.province = province;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }
}
900761252.400 ±(99.9%) 836615744.826 ns/op


Result "cn.wkq.java.string.StringInternTest.stringTestWithOutIntern":
  763138737.923 ±(99.9%) 930573867.253 ns/op [Average]
  (min, avg, max) = (439268202.215, 763138737.923, 1017594855.563), stdev = 241667078.162
  CI (99.9%): [0, 1693712605.176] (assumes normal distribution)


# Run complete. Total time: 00:03:34

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                                 (count)  Mode  Cnt           Score           Error  Units
StringInternTest.stringTestWithIntern       10000  avgt    5    13128831.111 ±   1999580.768  ns/op
StringInternTest.stringTestWithIntern      100000  avgt    5   157188187.749 ±  99748905.878  ns/op
StringInternTest.stringTestWithIntern     1000000  avgt    5  1813072765.600 ± 619163864.504  ns/op
StringInternTest.stringTestWithOutIntern    10000  avgt    5     1511080.000 ±    872905.808  ns/op
StringInternTest.stringTestWithOutIntern   100000  avgt    5     8238751.953 ±   1401387.637  ns/op
StringInternTest.stringTestWithOutIntern  1000000  avgt    5   763138737.923 ± 930573867.253  ns/op

Benchmark result is saved to result.json

References

[1] 性能调优必备利器之JMH
[2] openjdk/jmh/samples/
[3] code-tools/jmh
[4] jmh-core api