jmh的用法
什么是jmh呢?
JMH,全称Java Microbenchmark Harness,是一个用于测量Java和其他JVM(Java虚拟机)语言编写的基准测试的工具。这个工具由Java虚拟机团队开发,旨在提供一种简单且准确的方式来测量代码的运行性能。
支持多种测试模式,包括吞吐量(Throughput)、平均时间(AverageTime)等,用户可以根据需要选择合适的测试模式。
jmh入门
maven设置如下:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.27</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.27</version>
</dependency>GettingStartedTests.java代码如下:
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;
import java.util.concurrent.TimeUnit;
// https://blog.csdn.net/tanhongwei1994/article/details/120419321
// 吞吐量模式:每秒多少次调用
@BenchmarkMode(Mode.Throughput)
// 进行2次预热,每次预热持续1秒
@Warmup(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
// 进行3次基准测试,每次基准测试持续3秒
@Measurement(iterations = 3, time = 3, timeUnit = TimeUnit.SECONDS)
// 提供报告结果的默认时间单位
@OutputTimeUnit(TimeUnit.SECONDS)
// 断点调试时fork=0
@Fork(value = 1, jvmArgs = {"-Xmx2G", "-server"})
@Threads(1)
public class GettingStartedTests {
/**
*
*/
@Benchmark
public void test() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
}
/**
* @param args
* @throws RunnerException
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
// 指定运行的基准测试类
.include(GettingStartedTests.class.getSimpleName())
// 指定不运行的基准测试类
// .exclude(JMHSample_01_HelloWorld.class.getSimpleName())
// 发生错误停止测试
.shouldFailOnError(true)
.build();
new Runner(opt).run();
}
}编译入门例子
mvn package运行jmh测试
java -jar target/benchmark-jmh.jar或者通过IDEA选择测试GettingStartedTests点击右键运行测试
@State用法
JMH(Java Microbenchmark Harness)是Java的一个微基准测试工具套件,它允许开发人员对Java代码进行精确的性能测试。在JMH中,@State注解是一个非常重要的组成部分,它用于声明一个类作为基准测试的状态对象,并定义这个状态对象的共享范围。以下是@State注解用法的详细解析:
@State注解的作用
@State注解用于标记一个类,表明这个类是一个状态对象,可以在基准测试方法中被引用。- 它允许开发者控制状态对象的实例化方式以及在线程间的共享方式。
@State注解的参数
@State注解接受一个Scope枚举作为参数,用于指定状态对象的共享范围。Scope主要有以下几种:
Scope.Thread:- 每个测试线程都会有一个该状态对象的独立实例。
- 这意味着每个线程在访问状态对象时,都是操作自己的私有副本,不会影响到其他线程的状态。
- 这是默认的作用域。
Scope.Benchmark:- 所有测试线程都会共享同一个状态对象实例。
- 这适用于测试多线程环境下对共享资源的访问和修改,可以模拟并发访问共享资源的场景。
Scope.Group:- 同一个线程组内的所有线程会共享同一个状态对象实例。
- 同一线程组每个测试线程都会有一个该状态对象的独立实例。
- 线程组的概念在JMH中相对较为特殊,它允许开发者将多个基准测试方法分组,并在同一组内共享状态。
使用示例
下面是一个简单的使用@State注解的示例:
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
@State(Scope.Benchmark)
public class SharedState {
volatile double x = Math.PI;
}
@State(Scope.Thread)
public class UnsharedState {
volatile double x = Math.PI;
}
public class MyBenchmark {
@Benchmark
public void testSharedState(SharedState state) {
// 所有线程都会访问并修改同一个SharedState实例的x字段
state.x++;
}
@Benchmark
public void testUnsharedState(UnsharedState state) {
// 每个线程都会访问并修改自己独立的UnsharedState实例的x字段
state.x++;
}
}在这个示例中,SharedState类被标记为Scope.Benchmark,表示所有测试线程都会共享同一个SharedState实例。而UnsharedState类被标记为Scope.Thread,表示每个测试线程都会有一个独立的UnsharedState实例。
注意事项
- 在使用
@State注解时,需要确保状态对象中的字段是线程安全的,特别是在Scope.Benchmark和Scope.Group作用域下。 - 基准测试方法可以通过参数接收状态对象,JMH会在调用基准测试方法时自动注入相应的状态对象实例。
- 合理使用
@State注解可以大大提高基准测试的准确性和可靠性,帮助开发者更好地理解和优化Java代码的性能。
示例
@State详细用法示例代码请参考 链接
package com.future.demo.java;
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;
import java.util.concurrent.TimeUnit;
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
// 进行3次基准测试,每次基准测试持续3秒
@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
// 提供报告结果的默认时间单位
@OutputTimeUnit(TimeUnit.SECONDS)
public class JMHSample_03_States_Tests {
// https://blog.csdn.net/m0_37607945/article/details/111479890
// https://github.com/melix/jmh-gradle-example/blob/master/src/jmh/java/org/openjdk/jmh/samples/JMHSample_03_States.java
// 基础测试内线程间共享
//@State(Scope.Benchmark)
// 基准测试内线程内共享
//@State(Scope.Thread)
// 基础测试间变量共享
@State(Scope.Group)
public static class MyState {
public Object internalObject = new Object();
}
@Benchmark
@Group(value = "myGroup")
public void benchmark1(MyState state) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " ,internalObject=" + state.internalObject);
TimeUnit.SECONDS.sleep(1);
}
@Benchmark
@Group(value = "myGroup")
public void benchmark2(MyState state) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " ,internalObject=" + state.internalObject);
TimeUnit.SECONDS.sleep(1);
}
@Benchmark
@Group(value = "myGroup2")
public void benchmark3(MyState state) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " ,internalObject=" + state.internalObject);
TimeUnit.SECONDS.sleep(1);
}
/**
* @param args
* @throws RunnerException
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_03_States_Tests.class.getSimpleName())
// 2条并发执行线程
.threads(2)
// 断点调试时fork=0
.forks(1)
// 发生错误停止测试
.shouldFailOnError(true)
.build();
new Runner(opt).run();
}
}@Setup和@TearDown
@Setup和@TearDown的Level
在JMH(Java Microbenchmark Harness)中,Trial、Iteration和Invocation是理解其测试过程和执行机制的关键概念。它们各自代表了不同的测试层级和粒度,共同构成了JMH性能测试的基础。
- Trial
定义与作用:
- Trial是JMH性能测试中的最高层级,它代表了一次完整的基准测试过程。
- 在每次Benchmark测试之前或之后,可以定义一些操作作为Trial级别的操作,这些操作会在每次完整的测试过程之前或之后执行。
- Trial级别的操作允许开发者在测试前后进行资源的准备和清理工作,如数据库的初始化、缓存的清理等。
注意:
- Trial级别的操作不会直接影响测试结果,但它们是保证测试环境一致性和测试结果可靠性的重要手段。
- Iteration
定义与作用:
- Iteration是JMH性能测试中的一个重要层级,它代表了一次测试迭代过程。
- 在预热(Warmup)和测量(Measurement)阶段,都会进行多次Iteration来收集数据。
- 每次Iteration都包含了一组Invocations(即benchmark方法的多次调用)。
- Iteration是JMH进行测试的最小单位,它允许开发者在每次迭代前后执行一些操作,以观察不同迭代之间的性能变化。
注意:
- Iteration的次数、时长等参数可以通过JMH的注解进行配置,以满足不同的测试需求。
- 由于JVM的JIT(即时编译器)机制,多次迭代后代码的执行速度可能会发生变化,因此预热阶段是非常重要的。
- Invocation
定义与作用:
- Invocation是JMH性能测试中最细粒度的层级,它代表了一次benchmark方法的调用。
- 在每次Iteration中,会进行多次Invocations来收集足够的数据以计算性能指标(如吞吐量、平均时间等)。
- Invocation级别的操作允许开发者在每次方法调用前后执行一些操作,以观察方法调用的具体性能表现。
注意:
- Invocation是JMH性能测试中最直接反映代码性能的部分。
- 为了确保测试结果的准确性,需要避免在benchmark方法中进行无用的操作或调用外部资源。
总结
JMH通过Trial、Iteration和Invocation三个层级来组织性能测试过程,它们各自代表了不同的测试粒度和执行阶段。在实际使用中,开发者可以根据具体需求配置这些参数来进行性能测试,并通过分析测试结果来优化代码性能。同时,需要注意的是,为了确保测试结果的准确性和可靠性,需要避免在测试过程中引入不必要的噪音和偏差。
@Setup和@TearDown的用法
详细用法请参考 链接
示例:
package com.future.demo.java.samples;
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;
import java.util.concurrent.TimeUnit;
// https://blog.csdn.net/wyaoyao93/article/details/115727005
@Warmup(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
// 提供报告结果的默认时间单位
@OutputTimeUnit(TimeUnit.SECONDS)
// 断点调试时fork=0
@Fork(value = 1, jvmArgs = {"-Xmx2G", "-server"})
@Threads(2)
public class JMHSample_Setup_Teardown {
@State(Scope.Benchmark)
public static class MyStateBenchmark {
public Object internalObject;
// Trial是JMH性能测试中的最高层级,它代表了一次完整的基准测试过程。
// 在每次Benchmark测试之前或之后,可以定义一些操作作为Trial级别的操作,这些操作会在每次完整的测试过程之前或之后执行。
// Trial级别的操作允许开发者在测试前后进行资源的准备和清理工作,如数据库的初始化、缓存的清理等。
//@Setup(Level.Trial)
// Iteration是JMH性能测试中的一个重要层级,它代表了一次测试迭代过程。
// 在预热(Warmup)和测量(Measurement)阶段,都会进行多次Iteration来收集数据。
//@Setup(Level.Iteration)
// Invocation是JMH性能测试中最细粒度的层级,它代表了一次benchmark方法的调用。
@Setup(Level.Invocation)
public void setup() {
this.internalObject = new Object();
System.out.println("++++++++ benchmark setup, internalObject=" + this.internalObject + ", threadName=" + Thread.currentThread().getName());
}
//@TearDown(Level.Trial)
//@TearDown(Level.Iteration)
@TearDown(Level.Invocation)
public void teardown() {
System.out.println("++++++++ benchmark teardown, internalObject=" + this.internalObject + ", threadName=" + Thread.currentThread().getName());
}
}
@State(Scope.Thread)
public static class MyStateThread {
public Object internalObject;
//@Setup(Level.Trial)
//@Setup(Level.Iteration)
@Setup(Level.Invocation)
public void setup() {
this.internalObject = new Object();
System.out.println("++++++++ thread setup, internalObject=" + this.internalObject + ", threadName=" + Thread.currentThread().getName());
}
//@TearDown(Level.Trial)
//@TearDown(Level.Iteration)
@TearDown(Level.Invocation)
public void teardown() {
System.out.println("++++++++ thread teardown, internalObject=" + this.internalObject + ", threadName=" + Thread.currentThread().getName());
}
}
@Benchmark
public void benchmark1(MyStateBenchmark myStateBenchmark, MyStateThread myStateThread) throws InterruptedException {
System.out.println("myStateBenchmark.internalObject=" + myStateBenchmark.internalObject + ", myStateThread.internalObject=" + myStateThread.internalObject + ", threadName=" + Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
}
@Benchmark
public void benchmark2(MyStateBenchmark myStateBenchmark, MyStateThread myStateThread) throws InterruptedException {
System.out.println("myStateBenchmark.internalObject=" + myStateBenchmark.internalObject + ", myStateThread.internalObject=" + myStateThread.internalObject + ", threadName=" + Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_Setup_Teardown.class.getSimpleName())
// 发生错误停止测试
.shouldFailOnError(true)
.build();
new Runner(opt).run();
}
}jmh和spring集成
详细配置请参考https://gitee.com/dexterleslie/demonstration/blob/master/demo-java/demo-benchmark-jmh/src/test/java/com/future/demo/java/SpringIntegrationTests.java
示例:
package com.future.demo.java;
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;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.concurrent.TimeUnit;
/**
* jmh和spring集成
*/
@BenchmarkMode(Mode.Throughput)
@State(Scope.Benchmark) //使用的SpringBoot容器,都是无状态单例Bean,无安全问题,可以直接使用基准作用域BenchMark
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(1) //整体平均执行1次
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) //预热1s
@Measurement(iterations = 3, time = 30, timeUnit = TimeUnit.SECONDS) //测试也是1s、五遍
@Threads(-1)
public class SpringIntegrationTests {
TestService testService;
//springBoot容器
private ApplicationContext context;
public static void main(String[] args) throws RunnerException {
//使用注解之后只需要配置一下include即可,fork和warmup、measurement都是注解
Options opt = new OptionsBuilder()
.include(SpringIntegrationTests.class.getSimpleName())
// 断点调试时fork=0
.forks(1)
// 发生错误停止测试
.shouldFailOnError(true)
.jvmArgs("-Xmx2G",
"-server"/*,
"-XX:+UseG1GC",
"-XX:InitialHeapSize=8g",
"-XX:MaxHeapSize=8g",
"-XX:MaxGCPauseMillis=500",
"-XX:+DisableExplicitGC",
"-XX:+UseStringDeduplication",
"-XX:+ParallelRefProcEnabled",
"-XX:MaxMetaspaceSize=512m",
"-XX:MaxTenuringThreshold=1"*/)
.build();
new Runner(opt).run();
}
/**
* 初始化,获取springBoot容器,run即可,同时得到相关的测试对象
*/
@Setup(Level.Trial)
public void setup() {
//容器获取
context = SpringApplication.run(Application.class);
//获取对象
testService = context.getBean(TestService.class);
}
/**
* 测试的后处理操作,关闭容器,资源清理
*/
@TearDown(Level.Trial)
public void teardown() {
//使用子类ConfigurableApplicationContext关闭
((ConfigurableApplicationContext) context).close();
}
@Benchmark
public void test() {
this.testService.add(1, 2);
}
}参数化测试
注意:参数化测试中的参数在 @Setup 和 @TearDown 方法中也能够引用。
详细用法请参考示例https://gitee.com/dexterleslie/demonstration/blob/master/demo-java/demo-benchmark-jmh/src/test/java/com/future/demo/java/ParametersTests.java