浅谈Java基准性能测试之JMH

目录
  • 一、JMH vs JMeter
  • 二、JMH基本用法
    • 2.1、创建JMH项目
    • 2.2、编写基准测试代码
    • 2.3、JMH打包、运行
    • 2.4、JMH与Springboot
  • 三、JMH注解
    • 3.1、JMH Benchmark Modes
    • 3.2、Benchmark Time Units
    • 3.3、Benchmark State
    • 3.4、State Object @Setup @TearDown
    • 3.5、Fork
    • 3.6、Thread
    • 3.7、Warmup
    • 3.8、Measurement
  • 四、输出测试结果

一、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基本用法

2.1、创建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.
    }

}

2.2、编写基准测试代码

在上面生成的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();
    }
}

2.3、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是误差范围。

2.4、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/

3.1、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();
    }
}

3.2、Benchmark Time Units

JMH支持设置打印基准测试结果的时间单位,通过@OutputTimeUnit注解的方式设置。

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

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

}

3.3、Benchmark State

有时候我们在做基准测试的时候会需要使用一些变量、字段,@State注解是用来配置这些变量的生命周期,@State注解可以放在类上,然后在基准测试方法中可以通过参数的方式把该类对象作为参数使用。@State支持的生命周期类型:

  • Benchmark: 整个基准测试的生命周期,多个线程共用同一份实例对象。该类内部的@Setup @TearDown注解的方法可能会被任一个线程执行,但是只会执行一次。
  • Group: 每一个Group内部共享同一个实例,需要配合@Group @GroupThread使用。该类内部的@Setup @TearDown注解的方法可能会该Group内的任一个线程执行,但是只会执行一次。
  • Thread:每个线程的实例都是不同的、唯一的。该类内部的@Setup @TearDown注解的方法只会被当前线程执行,而且只会执行一次。

被@State标示的类必须满足如下两个要求:

类必须是public的

必须有无参构造函数

3.4、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: 每次调用测试的方法前/后都执行一次,这个执行频率会很高,一般用不上。

3.5、Fork

@Fork注解用来设置启动的JVM进程数量,多个进程是串行的方式启动的,多个进程可以减少偶发因素对测试结果的影响。

3.6、Thread

@Thread用来配置执行测试启动的线程数量

3.7、Warmup

@Warmup 用来配置预热的时间,如下所示配置预热五轮,每轮1second,也就是说总共会预热5s左右,在这5s内会不停的循环调用测试方法,但是预热时的数据不作为测试结果参考。

@Warmup(iterations = 5, time = 1)

3.8、Measurement

@Measurement用来配置基准测试的时间,如下所示配置预热10轮,每轮1second,也就是说总共会测试10s左右,在这10s内会不停的循环调用测试方法,同事测试数据会被基准测试结果参考。

@Measurement(iterations = 5, time = 1)

四、输出测试结果

jmh支持多种格式的结果输出text, csv, scsv, json, latex

如下打印出json格式的:

java -jar benchmark.jar -rf json

以上就是浅谈Java基准性能测试之JMH的详细内容,更多关于Java基准性能测试 JMH的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java中的StringBuilder性能测试

    在看KMP算法时,想要简单的统计一下执行时间和性能. 得出的结论是: Java的String的indexOf方法性能最好,其次是KMP算法,其次是传统的BF算法,当然,对比有点牵强,SUN的算法也使用Java来实现.用的看着不像是KMP,还需要详细研究一下. 测试代码如下所示: package com.test.test.kmp; import java.util.Random; public class KMPTest { public static void main(String[] ar

  • Java中Map的遍历方法及性能测试

    1. 阐述 对于Java中Map的遍历方式,很多文章都推荐使用entrySet,认为其比keySet的效率高很多.理由是:entrySet方法一次拿到所有key和value的集合:而keySet拿到的只是key的集合,针对每个key,都要去Map中额外查找一次value,从而降低了总体效率.那么实际情况如何呢? 为了解遍历性能的真实差距,包括在遍历key+value.遍历key.遍历value等不同场景下的差异,我试着进行了一些对比测试. 2. 对比测试 一开始只进行了简单的测试,但结果却表明k

  • Java常用排序算法及性能测试集合

    现在再回过头理解,结合自己的体会, 选用最佳的方式描述这些算法,以方便理解它们的工作原理和程序设计技巧.本文适合做java面试准备的材料阅读. 先附上一个测试报告: Array length: 20000bubbleSort : 766 msbubbleSortAdvanced : 662 msbubbleSortAdvanced2 : 647 msselectSort : 252 msinsertSort : 218 msinsertSortAdvanced : 127 msinsertSor

  • java8中parallelStream性能测试及结果分析

    测试1 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 20, time = 3, timeUnit = TimeUnit.SECONDS) @Fork(1) @State(Scope.Benchmark) public cla

  • 通过Java代码技巧改善性能

    前言 程序的性能受到代码质量的直接影响.这次主要介绍一些代码编写的小技巧和惯例.虽然看起来有些是微不足道的编程技巧,却可能为系统性能带来成倍的提升,因此还是值得关注的. 慎用异常 在Java开发中,经常使用try-catch进行错误捕获,但是try-catch语句对系统性能而言是非常糟糕的.虽然一次try-catch中,无法察觉到她对性能带来的损失,但是一旦try-catch语句被应用于循环或是遍历体内,就会给系统性能带来极大的伤害. 以下是一段将try-catch应用于循环体内的示例代码: @

  • Java编程中的性能优化如何实现

      String作为我们使用最频繁的一种对象类型,其性能问题是最容易被忽略的.作为Java中重要的数据类型,是内存中占据空间比较大的一个对象.如何高效地使用字符串,可以帮助我们提升系统的整体性能. 现在,我们就从String对象的实现.特性以及实际使用中的优化这几方面来入手,深入理解以下String的性能优化. 在这之前,首先看一个问题.通过三种方式创建三个对象,然后依次两两匹配,得出的结果是什么?答案留到最后揭晓. String str1 = "abc"; String str2 =

  • Java CPU性能分析工具代码实例

    这篇文章主要介绍了Java CPU性能分析工具代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 背景 有处理过生产问题的同学基本都能遇到系统忽然缓慢,CPU突然飙升,甚至整个应用请求不可用.当出现这种情况下,在不影响数据准确性的前提下,我们应该尽快导出jstack和内存信息,然后重启系统,尽快回复系统的可用性,避免用户体验过差.本文针对CPU飙升问题,提供该问题的排查思路,从而能够快速定位到某线程甚至某快代码导致CPU飙升,从而提供处理该

  • 浅谈Java基准性能测试之JMH

    目录 一.JMH vs JMeter 二.JMH基本用法 2.1.创建JMH项目 2.2.编写基准测试代码 2.3.JMH打包.运行 2.4.JMH与Springboot 三.JMH注解 3.1.JMH Benchmark Modes 3.2.Benchmark Time Units 3.3.Benchmark State 3.4.State Object @Setup @TearDown 3.5.Fork 3.6.Thread 3.7.Warmup 3.8.Measurement 四.输出测试

  • 浅谈java中OO的概念和设计原则(必看)

    一.OO(面向对象)的设计基础 面向对象(OO):就是基于对象概念,以对象为中心,以类和继承为构造机制,充分利用接口和多态提供灵活性,来认识.理解.刻划客观世界和设计.构建相应的软件系统.面向对象的特征:虽然各种面向对象编程语言相互有别,但都能看到它们对面向对象基本特征的支持, 即 "抽象.封装.继承.多态" : – 抽象,先不考虑细节 – 封装,隐藏内部实现 – 继承,复用现有代码 – 多态,改写对象行为 面向对象设计模式:是"好的面向对象设计",所谓"

  • 浅谈Java代理(jdk静态代理、动态代理和cglib动态代理)

    一.代理是Java常用的设计模式,代理类通过调用被代理类的相关方法,并对相关方法进行增强.加入一些非业务性代码,比如事务.日志.报警发邮件等操作. 二.jdk静态代理 1.业务接口 /** * 业务接口 * @author pc * */ public interface UserService { // 增加一个用户 public void addUser(); // 编辑账户 public void editUser(); } 2.业务实现类 /** * 业务实现类 * @author pc

  • 浅谈Java中常用数据结构的实现类 Collection和Map

    线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中.本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtable ├HashMap └WeakHashMap Collection接口 Collection是最基本的集合接口,一个C

  • 浅谈Java中各种修饰符与访问修饰符的说明

    JAVA中的类只能是public 或者package的.这是符合逻辑的:人们定义类的初衷就是为了让别人用的.倘若是private,别人怎么调用?但是有一个内部类可以被定义为private.严格上说,内部类,算不得上是一种光明正大的类,内部类在某种意义上是类这个王国里的特务和地下工作者.特务和地下工作者为王国起了不少作用,但是几乎从来不敢在公众场合抛投露面.就算要露面,也要在主人(class)的同意下,向导(Interface)的引导下,才敢战战兢兢的走出来.下面是常规的一些类的修饰符和访问修饰符

  • 浅谈java常用的几种线程池比较

    1. 为什么使用线程池 诸如 Web 服务器.数据库服务器.文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务.请求以某种方式到达服务器,这种方式可能是通过网络协议(例如 HTTP.FTP 或 POP).通过 JMS 队列或者可能通过轮询数据库.不管请求如何到达,服务器应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的. 构建服务器应用程序的一个简单模型是:每当一个请求到达就创建一个新线程,然后在新线程中为请求服务.实际上对于原型开发这

  • 浅谈java泛型的作用及其基本概念

    一.泛型的基本概念 java与c#一样,都存在泛型的概念,及类型的参数化.java中的泛型是在jdk5.0后出现的,但是java中的泛型与C#中的泛型是有本质区别的,首先从集合类型上来说,java 中的ArrayList<Integer>和ArrayList<String>是同一个类型,在编译时会执行类型擦除,及java中的类型是伪泛型,伪泛型将会在后面介绍,其次,对于像集合中添加基本类型的数据时,例如int,会首先将int转化成Integer对象,即我们通常所说的装箱操作,在取出

  • 浅谈java中异步多线程超时导致的服务异常

    在项目中为了提高大并发量时的性能稳定性,经常会使用到线程池来做多线程异步操作,多线程有2种,一种是实现runnable接口,这种没有返回值,一种是实现Callable接口,这种有返回值. 当其中一个线程超时的时候,理论上应该不 影响其他线程的执行结果,但是在项目中出现的问题表明一个线程阻塞,其他线程返回的接口都为空.其实是个很简单的问题,但是由于第一次碰到,还是想了一些时间的.很简单,就是因为阻塞的那个线 程没有释放,并发量一大,线程池数量就满了,所以其他线程都处于等待状态. 附上一段自己写的调

  • 浅谈Java自动装箱与拆箱及其陷阱

    在本文中,笔者向大家介绍下Java中一个非常重要也非常有趣的特性,就是自动装箱与拆箱,并从源码中解读自动装箱与拆箱的原理,同时这种特性也留有一个陷阱.开发者如果不注意,就会很容易跌入这个陷阱. 自动装箱(Autoboxing) 定义 大家在平时编写Java程序时,都常常以以下方式来定义一个Integer对象: Integer i=100; 从上面的代码中,大家可以得知,i为一个Integer类型的引用,100为Java中的基础数据类型(primitive data type).而这种直接将一个基

  • 浅谈Java中hashCode的正确求值方法

    本文研究的主要是Java中hashCode的正确求值方法的相关内容,具体如下. 散列表有一项优化,可以将对象的散列码(hashCode)缓存起来,如果散列码不匹配,就不会检查对象的等同性而直接认为成不同的对象.如果散列码(hashCode)相等,才会检测对象是否相等(equals). 如果对象具有相同的散列码(hashCode),他们会被映射到同一个散列桶中.如果散列表中所有对象的散列码(hashCode)都一样,那么该散列表就会退化为链表(linked list),从而大大降低其查询效率. 一

随机推荐