JMH Java 微基准测试套件

发布于 2024-06-11 00:12:35 字数 6073 浏览 15 评论 0

JMH (Java Microbenchmark Harness) 和 jMeter 的不同

JMH 和 jMeter 的使用场景还是有很大的不同的,jMeter 更多的是对 rest api 进行压测,而 JMH 关注的粒度更细,它更多的是发现某块性能槽点代码,然后对优化方案进行基准测试对比。比如 json 序列化方案对比,bean copy 方案对比,文中提高的洗牌算法对比等。

应用场景

JMH 比较典型的应用场景有:

  • 想准确的知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
  • 对比接口不同实现在给定条件下的吞吐量,找到最优实现
  • 查看多少百分比的请求在多长时间内完成

JMH 可视化

将测试例子结果的 json 文件导入,就可以实现可视化

JMH 插件

可以通过 IDEA 安装 JMH 插件使 JMH 更容易实现基准测试,在 IDEA 中点击 File->Settings…->Plugins,然后搜索 jmh,选择安装 JMH plugin,这个插件可以让我们能够以 JUnit 相同的方式使用 JMH,主要功能如下:

  • 自动生成带有 @Benchmark 的方法
  • 像 JUnit 一样,运行单独的 Benchmark 方法
  • 运行类中所有的 Benchmark 方

比如可以通过右键点击 Generate…,选择操作 Generate JMH benchmark 就可以生成一个带有 @Benchmark 的方法。还有将光标移动到方法声明并调用 Run 操作就运行一个单独的 Benchmark 方法。将光标移到类名所在行,右键点击 Run 运行,该类下的所有被 @Benchmark 注解的方法都会被执行。

使用

maven 引用

<!--   https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core   -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
<!-- <scope>test</scope>-->
</dependency>

JMH 基础

@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

为统计结果的时间单位,可用于类或者方法注解。例如 OutputTimeUnit 申明为纳秒,所以基准测试单位是 ns/op,即每次操作的纳秒单位平均时间
如果 @BenchmarkMode(Mode.Throughput) 和 @OutputTimeUnit(TimeUnit.MILLISECONDS) 那么基准测试结果就是每毫秒的吞吐量(即每毫秒多少次操作)

@Warmup

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

  • iterations:预热的次数
  • time:每次预热的时间
  • timeUnit:时间的单位,默认秒
  • batchSize:批处理大小,每次操作调用几次方法
//代码预热总计 5 秒(迭代 5 次,每次 1 秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)

为什么需要预热?

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

@Measurement

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

//表示循环运行 5 次,总计 5 秒时间
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)

@Threads

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

@Fork

进行 fork 的次数,可用于类或者方法上。如果 @Fork(1),那么就是一个线程,这时候就是同步模式。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。

@Param

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

@Param({"1","2","3"})
int outputType;
@Benchmark
public String benchmark() throws TemplateException, IOException {
if(outputType==3){
return doStream();
}else if(outputType==2) {
return doCharStream()
}else{
return doString();
}

}

@Setup 和 @TearDown

这是一对注解,作用于方法上,前者用于测试前的初始化工作,后者用于回收某些资源,比如压测前需要准备一些数据

@Level

用于控制 @Setup,@TearDown 的调用时机,有如下含义

  • Level.Tiral: 运行每个性能测试的时候执行,推荐的方式
  • Level.Iteration, 每次迭代的时候执行
  • Level.Invocation,每次调用方法的时候执行,这个选项需要谨慎使用

运行 Benchmark

JMH 提供了 Runner 类能运行 Benchmark 类


public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(StringConnectTest.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
//include 接受一个字符串表达式,如只测试方法名字包含“testObjectKey“的方法
include(MyBenchmark.class.getSimpleName()+".*testObjectKey*")

//用 4 个子进程做性能测试,每个进程预热一次,执行 5 次迭代
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(4)
.warmupIterations(1)
.measurementIterations(5)
.build();
new Runner(opt).run();
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

老街孤人

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

玍銹的英雄夢

文章 0 评论 0

我不会写诗

文章 0 评论 0

十六岁半

文章 0 评论 0

浸婚纱

文章 0 评论 0

qq_kJ6XkX

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文