胖胖的枫叶
主页
博客
产品设计
企业架构
全栈开发
效率工具
数据分析
项目管理
方法论
面试
  • openJdk-docs
  • spring-projects-docs
  • mysql-docs
  • redis-commands
  • redis-projects
  • apache-rocketmq
  • docker-docs
  • mybatis-docs
  • netty-docs
  • journaldev
  • geeksforgeeks
  • 后端进阶
  • 并发编程网
  • 英语肌肉记忆锻炼软件
  • 墨菲安全
  • Redisson-docs
  • jmh-Visual
  • 美团技术
  • MavenSearch
主页
博客
产品设计
企业架构
全栈开发
效率工具
数据分析
项目管理
方法论
面试
  • openJdk-docs
  • spring-projects-docs
  • mysql-docs
  • redis-commands
  • redis-projects
  • apache-rocketmq
  • docker-docs
  • mybatis-docs
  • netty-docs
  • journaldev
  • geeksforgeeks
  • 后端进阶
  • 并发编程网
  • 英语肌肉记忆锻炼软件
  • 墨菲安全
  • Redisson-docs
  • jmh-Visual
  • 美团技术
  • MavenSearch
  • 标签索引
  • 2024年

    • 配置Mac环境
    • 业务知识会计管理
    • 业务知识会计基础
    • 业务知识什么是财务
  • 2023年

    • 项目 Boi
  • 2022年

    • 企业架构故障管理
    • 企业架构开发债务
  • 2021年

    • Python3.8 Matplotlib员工数据分析
    • Python3.8 Matplotlib IP折线图
    • Python3.8 词云 IP地址
    • Redis RediSearch
    • Rust第一个CLI程序
    • Rust所有权
    • Rust函数与控制流
    • Rust变量与数据类型
    • Rust入门
    • 企业架构分布式系统
    • 编程式权限设计
    • Java JVM优化
    • SpringBoot MyBatis 批量
    • SpringBoot 测试Mock
    • SpringBoot Redis布隆过滤器
    • CentOS7 Jenkins 部署
    • SpringBoot WebClient
    • Docker Drone 部署
    • SpringBoot MyBatis
    • SpringBoot Redisson
    • SpringBoot MyBatis 雪花算法
    • Java Netty
    • Redis 扫描
    • CentOS7 Jenkins本地部署分级
    • Mac 安装 Neo4j Jupyter
    • Mac OpenJDK11 JavaFX 环境
    • Mac 安装 Jenv
    • SpringBoot Redis 延时队列
    • SpringBoot MDC日志
    • SpringBoot 定时任务
    • CentOS7 Nginx GoAccess
    • SpringBoot MyBatis 分析
    • SpringBoot Lucene
    • 企业架构分布式锁
    • 学习技巧减少学习排斥心理
    • SpringBoot 动态数据源
    • Docker Compose SpringBoot MySQL Redis
    • SpringBoot 阻塞队列
    • Docker Compose Redis 哨兵
    • Docker Compose Redis 主从
    • 网络通信
  • 2020年

    • SpringBoot 延时队列
    • MySQL基础(四)
    • Java 雪花算法
    • Redis Geo
    • 网络通信 Tcpdump
    • Spring SPI
    • Java Zookeeper
    • SpringBoot JMH
    • 网络通信 Wireshark
    • Docker Compose Redis MySQL
    • CentOS7 Docker 部署
    • Netty 源码环境搭建
    • MySQL基础(三)
    • CentOS7 Selenium运行环境
    • CentOS7 Nginx HTTPS
    • Java JMH
    • SpringBoot 修改Tomcat版本
    • Java Eureka 钉钉通知
    • SpringBoot 错误钉钉通知
    • Java JVM
    • Git 合并提交
    • CentOS7 OpenResty 部署
  • 2019年

    • Redis CLI
    • CentOS7 Nginx 日志
    • 编程式代码风格
    • IDEA 插件
    • Skywalking 源码环境搭建
    • SpringBoot Redis 超时错误
    • 编程式 gRPC
    • Java Arthas
    • Docker Compose Redis 缓存击穿
    • Docker ElasticSearch5.6.8 部署
    • Docker Mysql5.7 部署
    • Spring Redis 字符串
    • Docker Zookeeper 部署
    • Docker Redis 部署
    • SpringBoot Dubbo
    • CentOS7 CMake 部署
    • 应用程序性能指标
    • Java Code 递归
    • CentOS7 ELK 部署
    • CentOS7 Sonarqube 部署
    • Java Selenium
    • Java JJWT JUnit4
    • Spring 源码环境搭建
    • Java JUnit4
    • Java Web JSON Token
    • 编程式 FastDFS
    • Java XPath
    • Redis基础(二)
    • Redis基础(一)
    • Java MyBatis JUnit4
    • Java MyBatis H2 JUnit4
    • MyBatis 源码环境搭建
    • Git 配置
    • Java 核心
    • Java Dubbo
    • Java JavaCollecionsFramework
    • Java Maven
    • Java MyBatis
    • Java Spring
    • Java SpringMVC
    • MySQL
    • Redis
  • 2018年

    • Java HashMap
    • Java HashSet
    • Java Code 交换值
    • Spring Upgrade SpringBoot
    • Mac 编程环境
    • Java Log4j
    • 网络通信 Modbus
    • MySQL基础(二)
    • MySQL基础(一)
    • Java Stack
    • Java Vector
    • CentOS7 RabbitMQ 部署
    • CentOS7 Redis 部署
    • CentOS7 MongoDB 部署
    • CentOS7 基础命令
    • Java Eureka Zookeeper
    • CentOS7 MySQL 部署
    • Git 分支
    • CentOS7 Java环境配置
    • Java LinkedList
    • Java ArrayList
    • Spring Annotation Aop

Java JMH

JMH

JMH(Java Microbenchmark Harness)是由OpenJDK Developer提供的基准测试工具(基准可以理解为比较的基础,我们将这一次性能测试结果作为基准结果,下一次的测试结果将与基准数据进行比较),它是一种常用的性能测试工具,解决了基准测试中常见的一些问题。

JMH结果分析

结果日志解释

  • 基础信息,显示Java路径、Java版本以及JMH基础配置信息
# JMH version: 1.21
# VM version: JDK 1.8.0_275, OpenJDK 64-Bit Server VM, 25.275-b01
# VM invoker: /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/jre/bin/java
# VM options: -server
# Warmup: 1 iterations, 10 s each
# Measurement: 1 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: cn.z201.jmh.ListBenchmark.testArrayList
# Parameters: (size = 100)
  • 预热次数。预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,被测代码应该得到了充分的 JIT 编译和优化。
# Warmup Iteration   1: 0.002 ±(99.9%) 0.001 ms/op
Iteration   1: 0.002 ±(99.9%) 0.001 ms/op

Result "cn.z201.jmh.ListBenchmark.testArrayList":
  0.002 ms/op
  • 结果表明,在拼接字符次数越多的情况下,LinkedList.add() 的性能就更好。这是得益于LinkedList双向链表结构,每次add都是在最后一个位置添加元素。
Benchmark                               (size)  Mode  Cnt    Score   Error  Units
ListBenchmark.testArrayList                100  avgt         0.002          ms/op
ListBenchmark.testArrayList               1000  avgt         0.018          ms/op
ListBenchmark.testArrayList              10000  avgt         0.194          ms/op
ListBenchmark.testCopyOnWriteArrayList     100  avgt         0.015          ms/op
ListBenchmark.testCopyOnWriteArrayList    1000  avgt         1.386          ms/op
ListBenchmark.testCopyOnWriteArrayList   10000  avgt       148.161          ms/op
ListBenchmark.testLinkedList               100  avgt         0.002          ms/op
ListBenchmark.testLinkedList              1000  avgt         0.017          ms/op
ListBenchmark.testLinkedList             10000  avgt         0.163          ms/op

注解使用

@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 注解。
  • 如果只是用@Param在编译时会报错,它必须配合@State注解使用,@State指定了对象共享范围。
    • @State(value = Scope.Benchmark):基准测试内共享对象
    • @State(value = Scope.Group):同一个线程组内共享
    • @State(value = Scope.Thread):同一个线程内共享

@Setup & @TearDown

  • 假如初始化和销毁代码并不是基准测试的一部分,为了减少测试噪,音所以不应该放到@Benchmark修饰的方法内部,JMH提供了@Setup和@TearDown实现这样的功能。

案例一

比较 String.join、StringBuilder.append 、StringBuffer.append参数

  • 分别添加100、1000、10000次字符串。

演示代码

package cn.z201.jmh;
/**
 * @author z201.coding@gmail.com
 **/
@State(Scope.Benchmark)
public class StringBenchmark {
    private final static Integer MEASUREMENT_ITERATIONS = 1;
    private final static Integer WARMUP_ITERATIONS = 1;

    @Param(value = {"100","1000","10000"})
    private int size;

    public List<Integer> stringList = null;

 @Setup(Level.Trial)
    public synchronized void initialize() {
        try {
            stringList = new Random()
                    .ints()
                    .limit(size)
                    .boxed()
                    .map(i->i.toString())
                    .collect(Collectors.toList());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Throughput 整体吞吐量,例如”1秒内可以执行多少次调用”。
    // AverageTime: 调用的平均时间,例如”每次调用平均耗时xxx毫秒”。
    // SampleTime: 随机取样,最后输出取样结果的分布,例如”99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
    // SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
    // All(“all”, “All benchmark modes”);
    @Test
    public void executeJmhRunner() throws RunnerException {
        Options opt = new OptionsBuilder()
                // 设置类名正则表达式基准为搜索当前类
                .include("\\." + StringBenchmark.class.getSimpleName() + "\\.")
                // 都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。
                .warmupIterations(WARMUP_ITERATIONS) // 多少次预热
                .measurementIterations(MEASUREMENT_ITERATIONS) // 要做多少次测量m
                .timeUnit(TimeUnit.MILLISECONDS) // 毫秒
                // 不使用多线程
                .forks(0) // 进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
                .threads(8) // 每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为cpu乘以2。
                .mode(Mode.AverageTime)
                .shouldDoGC(true)
                .shouldFailOnError(true)
                .resultFormat(ResultFormatType.JSON) // 输出格式化
//                .result("/dev/null") // set this to a valid filename if you want reports
                .result("benchmark.json")
                .shouldFailOnError(true)
                .jvmArgs("-server")
                .build();
        new Runner(opt).run();
    }

    @Benchmark
    public void testStringJoin(Blackhole blackhole){
        String str = new String();
        stringList.stream().forEach(i-> str.join(i.toString()));
        blackhole.consume(str);
    }

    @Benchmark
    public void testStringBuffer(Blackhole blackhole){
        StringBuffer stringBuffer = new StringBuffer();
        stringList.stream().forEach(i-> stringBuffer.append(i.toString()));
        blackhole.consume(stringBuffer);
    }

    @Benchmark
    public void testStringBuilder(Blackhole blackhole){
        StringBuilder stringBuilder = new StringBuilder();
        stringList.stream().forEach(i-> stringBuilder.append(i.toString()));
        blackhole.consume(stringBuilder);
    }
}

案例二

比较ArrayList.add 、LinkedList.add、CopyOnWriteArrayList.add

  • 分别添加100、1000、10000次

演示代码

/**
 * @author z201.coding@gmail.com
 **/
@State(Scope.Benchmark)
public class ListBenchmark {

    private final static Integer MEASUREMENT_ITERATIONS = 1;
    private final static Integer WARMUP_ITERATIONS = 1;

    @Param(value = {"100","1000","10000"})
    private int size;

    public List<String> stringList = null;

    @Setup(Level.Trial)
    public synchronized void initialize() {
        try {
            stringList = new Random()
                    .ints()
                    .limit(size)
                    .boxed()
                    .map(i->i.toString())
                    .collect(Collectors.toList());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Throughput 整体吞吐量,例如”1秒内可以执行多少次调用”。
    // AverageTime: 调用的平均时间,例如”每次调用平均耗时xxx毫秒”。
    // SampleTime: 随机取样,最后输出取样结果的分布,例如”99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
    // SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
    // All(“all”, “All benchmark modes”);
    @Test
    public void executeJmhRunner() throws RunnerException {
        Options opt = new OptionsBuilder()
                // 设置类名正则表达式基准为搜索当前类
                .include("\\." + getClass().getSimpleName() + "\\.")
                // 都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。
                .warmupIterations(WARMUP_ITERATIONS) // 多少次预热
                .measurementIterations(MEASUREMENT_ITERATIONS) // 要做多少次测量m
                .timeUnit(TimeUnit.MILLISECONDS) // 毫秒
                // 不使用多线程
                .forks(0) // 进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
                .threads(8) // 每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为cpu乘以2。
                .mode(Mode.AverageTime)
                .shouldDoGC(true)
                .shouldFailOnError(true)
                .resultFormat(ResultFormatType.JSON) // 输出格式化
//                .result("/dev/null") // set this to a valid filename if you want reports
                .result("ListBenchmark.json")
                .shouldFailOnError(true)
                .jvmArgs("-server")
                .build();
        new Runner(opt).run();
    }

    @Benchmark
    public void testArrayList(Blackhole blackhole){
        List<String> arrayList = new ArrayList<>();
        stringList.stream().forEach(i->arrayList.add(i));
        blackhole.consume(arrayList);
    }

    @Benchmark
    public void testLinkedList(Blackhole blackhole){
        List<String> linkedList = new LinkedList<>();
        stringList.stream().forEach(i->linkedList.add(i));
        blackhole.consume(linkedList);
    }

    @Benchmark
    public void testCopyOnWriteArrayList(Blackhole blackhole){
        List<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
        stringList.stream().forEach(i->copyOnWriteArrayList.add(i));
        blackhole.consume(copyOnWriteArrayList);
    }
}

案例三

JMH 官方提供了生成 jar 包的方式来执行

  • 对比AtomicLong 、VolatileLong 、log求和

打包项目

<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</finalName>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>cn.z201.jmh.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

测试代码


/**
 * @author z201.coding@gmail.com
 **/
@State(Scope.Benchmark)
public class Main {

    private final static Integer MEASUREMENT_ITERATIONS = 1;
    private final static Integer WARMUP_ITERATIONS = 1;

    @Param(value = {"100", "1000", "10000"})
    private int size;

    public List<Integer> numberList = null;

    @Setup(Level.Trial)
    public synchronized void initialize() {
        try {
            numberList = new Random()
                    .ints()
                    .limit(size)
                    .boxed()
                    .collect(Collectors.toList());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                // 设置类名正则表达式基准为搜索当前类
                .include("\\." + Main.class.getSimpleName() + "\\.")
                // 都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。
                .warmupIterations(WARMUP_ITERATIONS) // 多少次预热
                .measurementIterations(MEASUREMENT_ITERATIONS) // 要做多少次测量m
                .timeUnit(TimeUnit.MILLISECONDS) // 毫秒
                // 不使用多线程
                .forks(0) // 进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
                .threads(8) // 每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为cpu乘以2。
                .mode(Mode.AverageTime)
                .shouldDoGC(true)
                .shouldFailOnError(true)
                .resultFormat(ResultFormatType.JSON) // 输出格式化
//                .result("/dev/null") // set this to a valid filename if you want reports
                .result("Main.json")
                .shouldFailOnError(true)
                .jvmArgs("-server")
                .build();
        new Runner(opt).run();
    }

    public static class VolatileLong {
        private volatile long value = 0;

        public synchronized void add(long amount) {
            this.value += amount;
        }

        public long getValue() {
            return this.value;
        }
    }

    @Benchmark
    public void atomicLong(Blackhole blackhole) {
        AtomicLong atomicLong = new AtomicLong();
        numberList.parallelStream().forEach(atomicLong::addAndGet);
        blackhole.consume(atomicLong.get());
    }

    @Benchmark
    public void volatileLong(Blackhole blackHole) {
        VolatileLong volatileLong = new VolatileLong();
        numberList.parallelStream().forEach(volatileLong::add);
        blackHole.consume(volatileLong.getValue());
    }

    @Benchmark
    public void longStreamSum(Blackhole blackHole) {
        long sum = numberList.parallelStream().mapToLong(s -> s).sum();
        blackHole.consume(sum);
    }

}

启动

➜  JMH git:(main) ✗ cd target 
➜  target git:(main) ✗ java -jar jmh.jar 

END

最近更新: 2025/12/27 18:51
Contributors: 庆峰
Prev
CentOS7 Nginx HTTPS
Next
SpringBoot 修改Tomcat版本