当前位置: 首页 > ops >正文

Java开源工具库使用之性能测试JMH

文章目录

  • 前言
  • 一、配置参数
    • 1.1 注解列表
    • 1.2 非注解配置
    • 1.3 概念
  • 二、简单例子
    • 2.1 throught(吞吐量)
    • 2.2 ArrayList vs Set
    • 2.3 StringBuilder vs StringBuffer
    • 2.4 Stream vs parallelStream vs for
  • 三、结果可视化
  • 四、IDEA插件
  • 参考

前言

JMH(Java Microbenchmark Harness),是 OpenJDK 团队开发的一款基准测试工具,一般用于代码的性能比较和调优,精度甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。

github地址:https://github.com/openjdk/jmh

官方使用例子:https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples

pom 依赖:

<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.35</version>
</dependency>
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.35</version>
</dependency>

一、配置参数

1.1 注解列表

注解名作用域作用
AuxCountersTYPE辅助计数器,可以统计 @State 修饰的对象中的 public 属性被执行的情况。实验性API,将来可能删除
BenchmarkMETHOD标记为基准测试方法,和 junit @Test 类似
BenchmarkModeTYPE,METHOD指明了基准测试的模式, 模式可以任意组合,详细见下方
CompilerControlTYPE,METHOD,CONSTRUCTOR编译控制选项,是否使用编译优化
ForkTYPE,METHODfork出 jvm 子进程进行测试,一般设置为1
GroupMETHOD控制多线程组
GroupThreadsMETHOD设置参与组的线程数量
MeasurementTYPE,METHOD设置默认测量参数
OperationsPerInvocationTYPE,METHOD设置单个 benchmark 方法 op 个数(默认1个benchmark一个op)
OutputTimeUnitTYPE.METHOD指定输出的时间单位,可以传入 java.util.concurrent.TimeUnit 中的时间单位,最小可以到纳秒级别
ParamFIELD允许使用一个 Benchmark 方法跑多组数据,特别适合测量方法性能和参数取值的关系
SetupMETHOD用于基准测试前的初始化动作,可通过参数 level 确定粒度, 具体见下方
StateTYPE声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围, 当使用 @Setup 的时候,必须在类上加这个参数,不然会提示无法运行。参数设置见下方
TearDownMETHOD用于基准测试后的动作
ThreadsTYPE,METHOD设置线程数
TimeoutTYPE,METHOD设置默认超时参数,java.util.concurrent.TimeUnit
WarmupTYPE.METHOD设置默认预热参数,详细见下方
  • AuxCounters

    • Type.EVENTS: 统计发生的次数
    • Type.OPERATIONS:按指定的格式统计,如按吞吐量统计
  • BenchmarkMode

    • Mode.Throughput :吞吐量,单位时间内执行的次数,默认值
    • Mode.AverageTime:平均时间,一次执行需要的单位时间,其实是吞吐量的倒数
    • Mode.SampleTime:是基于采样的执行时间,采样频率由JMH自动控制,同时结果中也会统计出p90、p95的时间
    • Mode.SingleShotTime:单次执行时间,只执行一次,可用于冷启动的测试
  • CompilerControl

    • Mode.BREAK:在生成的编译代码插入断点

    • Mode.PRINT:打印方法及配置文件

    • Mode.EXCLUDE:从编译中排除该方法

    • Mode.INLINE:强制使用内联

    • Mode.DONT_INLINE:强制跳过内联

    • Mode.COMPILE_ONLY:仅仅编译方法,其它啥都不干

  • Measurement

    • iterations:测量迭代次数,不是方法执行次数
    • time:每次迭代时间
    • timeUnit:时间单位
    • batchSize:一次迭代方法需要执行次数
  • Setup/TearDown

    • Level.Trial:Benchmark级别
    • Level.Iteration:执行迭代级别
    • Level.Invocation:每次方法调用级别
  • State

    • Scope.Thread:作用域为线程
    • Scope.Benchmark:作用域为本次JMH测试,线程共享
    • Scope.Group:作用域为组
  • WarmUp

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

    • iterations:预热迭代次数
    • time:每次迭代时间
    • timeUnit:时间单位
    • batchSize:一次迭代方法需要执行次数

1.2 非注解配置

除了上述使用注解进行配置,还有一些参数可以通过 OptionsBuilder 这个类进行配置

  • include 配置参与基准测试的类,参数是类的简单名称,不包含包名
  • exclude 排除的方法名,include 会默认导入所有 @Benchmark public 方法
  • addProfiler 添加分析器,能够得到更多关于 jvm 的信息。jmh 自身提供了很多分析器:如 GCProfiler, StackProfiler, ClassloaderProfiler 等等
  • detectJvmArgs 从父jvm检测参数,会覆盖jvmArgs
  • jvmArgs fork jvm 参数
  • shouldDoGC 在mesurement 迭代之间是否GC
  • verbosity 控制输出信息的级别

1.3 概念

  • JMH使用OPS来表示吞吐量,OPS,Opeartion Per Second,是衡量性能的重要指标,指得是每秒操作量。数值越大,性能越好。类似的概念还有TPS,表示每秒的事务完成量,QPS,每秒的查询量。
  • 如果对每次执行时间进行升序排序,取出总数的99%的最大执行时间作为 p99 的值,p99 通常是衡量系统性能重要指标,表示99%的请求的响应时间不超过某个值,类似的还有p95,p90, p999
  • 测试时间,测试时间 = (测试方法数量) * (warmup迭代次数 * 时间 + measurement迭代次数 * 时间) * (@Param参数个数的乘积) * (forks)

二、简单例子

2.1 throught(吞吐量)

先用一个最简单的例子做测试, 计算 testThrought 这个方法1s执行多少次,就是计算吞吐量

package com.aabond.demo.jmh;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;public class JmhDemo {@Benchmarkpublic void testThrought() throws InterruptedException {Thread.sleep(1000);}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(JmhDemo.class.getSimpleName()).forks(1).build();new Runner(opt).run();}}
Benchmark              Mode  Cnt  Score   Error  Units
JmhDemo.testThrought  thrpt    5  0.991 ± 0.004  ops/s

代码中,可以看出 testThrought 肯定会耗费1s左右的时间,结果正如所预料的一样。

上述代码只是用了最基本的默认配置,更多参数配置可以通过注解和代码来控制。

  • 默认的预热和迭代次数都是5,可以用@Warmup和@Measurement来自定义
  • 默认输出时间单位是秒,也可以用@OutputTimeUnit 实现显示其它单位
  • 默认基准测试是输出吞吐量,可以用@BenchmarkMode 设置平均时间,这两者互为倒数
@Warmup(iterations = 3)
@Measurement(iterations = 6)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {@Benchmarkpublic void testThrought() throws InterruptedException {Thread.sleep(1000);}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(JmhDemo.class.getSimpleName()).forks(1).build();new Runner(opt).run();}}
Benchmark             Mode  Cnt     Score   Error  Units
JmhDemo.testThrought  avgt    6  1009.491 ± 4.268  ms/op

2.2 ArrayList vs Set

ArrayList和 Set 的查找时间复杂度分别是 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1),利用 jmh 测试一下差距

使用 datafaker 准备100,1000,10000个中文姓名字符串, 再用100个随机中文姓名字符串进行查找,用这种方法进行测试

@Warmup(iterations = 3)
@Measurement(iterations = 6)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {List<String> names = new ArrayList<>();Set<String> namesSet = new HashSet<>();List<String> finds = new ArrayList<>();@Param({"100", "1000", "10000"})int originLen;@Setuppublic void setUp() {Faker faker = new Faker(Locale.CHINA);names = faker.collection(() -> faker.name().name()).len(originLen).generate();namesSet = new HashSet<>(names);finds =  faker.collection(() -> faker.name().name()).len(100).generate();}@Benchmark@OperationsPerInvocation(100)public boolean listFind() {for (String name: finds) {boolean b = names.contains(name);}return true;}@Benchmark@OperationsPerInvocation(100)public boolean setFind() {for (String name: finds) {boolean b = namesSet.contains(name);}return true;}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(JmhDemo.class.getSimpleName()).forks(1).build();new Runner(opt).run();}}
Benchmark         (originLen)  Mode  Cnt   Score    Error  Units
JmhDemo.listFind          100  avgt    610⁻⁴           ms/op
JmhDemo.listFind         1000  avgt    6   0.004 ±  0.001  ms/op
JmhDemo.listFind        10000  avgt    6   0.063 ±  0.006  ms/op
JmhDemo.setFind           100  avgt    610⁻⁵           ms/op
JmhDemo.setFind          1000  avgt    610⁻⁵           ms/op
JmhDemo.setFind         10000  avgt    610⁻⁵           ms/op

从结果可以看出,List 随着数据量的增大查找的速度逐渐变慢,数据量从 1 0 2 10^2 102-> 1 0 3 10^3 103-> 1 0 4 10^4 104, 查找耗费时间 1 0 − 4 10^{-4} 104-> 1 0 − 3 10^{-3} 103-> 1 0 − 2 10^{-2} 102

而 set 一直保持不变,保持在 1 0 − 5 10^{-5} 105

2.3 StringBuilder vs StringBuffer

StringBuilder 和 StringBuffer 都可以用来拼接字符串。一个是线程不安全的,而另一个是线程安全的。实际用 StringBuilder 用的比较多,想知道这两者的差异,用 jmh 来比较下速度。

@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 6, time = 5)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {List<String> names = new ArrayList<>();@Param({"1000", "100000", "10000000"})int originLen;@Setuppublic void setUp() {Faker faker = new Faker(Locale.CHINA);names = faker.collection(() -> faker.name().name()).len(originLen).generate();}@Benchmarkpublic void stringBufferAppend(Blackhole bh) {StringBuffer sb = new StringBuffer();for (String name: names) {sb.append(name);}bh.consume(sb);}@Benchmarkpublic void stringBuilderAppend(Blackhole bh) {StringBuilder sb = new StringBuilder();for (String name: names) {sb.append(name);}bh.consume(sb);}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(JmhDemo.class.getSimpleName()).build();new Runner(opt).run();}}
Benchmark                    (originLen)  Mode  Cnt    Score   Error  Units
JmhDemo.stringBufferAppend          1000  avgt    6    0.016 ± 0.002  ms/op
JmhDemo.stringBufferAppend        100000  avgt    6    1.635 ± 0.363  ms/op
JmhDemo.stringBufferAppend      10000000  avgt    6  162.367 ± 3.866  ms/op
JmhDemo.stringBuilderAppend         1000  avgt    6    0.015 ± 0.003  ms/op
JmhDemo.stringBuilderAppend       100000  avgt    6    1.701 ± 1.013  ms/op
JmhDemo.stringBuilderAppend     10000000  avgt    6  150.966 ± 4.673  ms/op

从结果看,StringBuffer和StringBuilder 拼接字符串的效率相差不大

2.4 Stream vs parallelStream vs for

@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 6, time = 5)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {@Param({"100000", "1000000", "10000000", "100000000"})int originLen;@Benchmarkpublic void streamGenerate(Blackhole bh) {int[] array = IntStream.range(0, originLen).toArray();bh.consume(array);}@Benchmarkpublic void streamParallelGenerate(Blackhole bh) {int[] array = IntStream.range(0, originLen).parallel().toArray();bh.consume(array);}@Benchmarkpublic void forGenerate(Blackhole bh) {int [] array = new int[originLen];for (int i = 0; i < originLen; i++) {array[i] = i;}bh.consume(array);}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(JmhDemo.class.getSimpleName()).result("E:\\list.json").resultFormat(ResultFormatType.JSON).build();new Runner(opt).run();}}
Benchmark                       (originLen)  Mode  Cnt    Score     Error  Units
JmhDemo.forGenerate                  100000  avgt    6    0.068 ±   0.015  ms/op
JmhDemo.forGenerate                 1000000  avgt    6    0.653 ±   0.105  ms/op
JmhDemo.forGenerate                10000000  avgt    6    6.581 ±   1.462  ms/op
JmhDemo.forGenerate               100000000  avgt    6   85.288 ± 104.922  ms/op
JmhDemo.streamGenerate               100000  avgt    6    0.098 ±   0.013  ms/op
JmhDemo.streamGenerate              1000000  avgt    6    0.916 ±   0.456  ms/op
JmhDemo.streamGenerate             10000000  avgt    6   26.783 ±   3.412  ms/op
JmhDemo.streamGenerate            100000000  avgt    6  191.738 ± 173.429  ms/op
JmhDemo.streamParallelGenerate       100000  avgt    6    0.104 ±   0.004  ms/op
JmhDemo.streamParallelGenerate      1000000  avgt    6    0.622 ±   0.022  ms/op
JmhDemo.streamParallelGenerate     10000000  avgt    6   23.461 ±  11.200  ms/op
JmhDemo.streamParallelGenerate    100000000  avgt    6   65.758 ±  44.910  ms/op

从结果看, for的消耗时间随着数据量增大而同比增大,成正比关系。而在千万数据上,流的性能突然下降,数据在亿级别,并行流性能更好

三、结果可视化

jmh 可通过将结果导出json数据

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

可以将Json数据上传两个网站,将结果可视化

  • https://jmh.morethan.io/

    在这里插入图片描述

  • http://deepoove.com/jmh-visual-chart/
    在这里插入图片描述

四、IDEA插件

IDEA 提供插件 JMH Java Microbenchmark Harness,能够使用快捷键 Alt+Insert 或 MacOS Ctrl + N快速生成测试方法,还可以执行单个方法,类似 junit

在这里插入图片描述

参考

  1. java中的即时编译(JIT)简介
  2. jmh使用
  3. 基准测试神器JMH——详解36个官方例子
  4. JAVA拾遗 — JMH与8个代码陷阱
http://www.xdnf.cn/news/11389.html

相关文章:

  • windows 2000 系统安装和配置
  • 【SQL注入】(1)原理,框架
  • elk logstash 详解
  • 网站建设经验分享:如何进行网站内容更新与维护?
  • 缓冲区(buffer)与缓存(cache)
  • 关于 ByteHouse 你想知道的一切,看这一篇就够了
  • 软件开发面试题(C#语言,.NET框架)
  • TextMate 小小心得
  • windows socket函数详解
  • WDA学习(25):DateNavigator使用
  • Android 三方APP调用系统隐藏API
  • 什么是可视化编程?为什么它如此重要?
  • 电脑C盘不知不觉满了?学会这6种解决方法!
  • 51 单片机基础
  • 手把手教你Apache2.4 + PHP8.39的安装(windows)及避坑问题点
  • 字节跳动-后台开发岗 面经
  • Model、Map、ModelAndView、HttpServletRequest区别
  • 更好的Java虚拟机Zing: 更好的性能,无停顿,更快的启动
  • 腾讯云 AI 代码助手保姆级使用教程
  • Sql-server 2008的安装
  • fdsfds
  • Git克隆操作
  • 【抓包工具】HttpWatch(功能详细介绍)
  • PulseAudio 设计和实现浅析
  • APB协议解读及历代协议对比
  • System.currentTimeMillis()用法以及计算方式
  • 医学案例|配对样本t检验
  • Java IO流操作(FileInputStream、ByteArrayInputStream、ObjectInputStream)
  • flash 小游戏大全
  • Android studio ListView应用设计