JMH - Java性能测试
JMH - Java性能测试
JMH 的全名是 Java Microbenchmark Harness,它是由 Java 虚拟机团队开发的一款用于 Java 微基准测试工具。用自己开发的工具测试自己开发的另一款工具,以子之矛,攻子之盾果真手到擒来,如臂使指。使用 JMH 可以让你方便快速的进行一次严格的代码基准测试,并且有多种测试模式,多种测试维度可供选择;而且使用简单、增加注解便可启动测试。
引入依赖
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.33</version>
<scope>provided</scope>
</dependency>
使用示例
@BenchmarkMode(Mode.Throughput)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
public class BenchmarkTest {
@Benchmark
public void test() throws Exception {
Thread.sleep(100);
System.out.println("1");
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(BenchmarkTest.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
运行结果:

与SpringBoot整合
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 1, time = 5)
@Measurement(iterations = 2, time = 5)
public class BenchmarkTest {
private ConfigurableApplicationContext context;
private NoteService noteService;
@Setup
public void setup() {
context = SpringApplication.run(SpringBootEsApplication.class);
noteService = context.getBean(NoteService.class);
}
@TearDown
public void tearDown() {
context.close();
}
@Benchmark
public void testMysql() throws Exception {
List<NoteResponse> list = noteService.searchByMysql("redis");
}
@Benchmark
public void testEs() throws Exception {
List<NoteResponse> list = noteService.searchByEs("redis");
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(BenchmarkTest.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
API
@BenchmarkMode
基准测试类型。
| 类型 | 说明 |
|---|---|
| Throughput | 整体吞吐量,例如“1 秒内可以执行多少次调用”。 |
| AverageTime | 调用的平均时间,例如“每次调用平均耗时 xxx 毫秒”。 |
| SampleTime | 随机取样,最后输出取样结果的分布,例如“99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内”。 |
| SingleShotTime | 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。 |
| All | 所有模式。 |
@State
当使用 @Setup 参数的时候,必须在类上加这个参数,不然会提示无法运行。
State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。
| Scope | 说明 |
|---|---|
| Thread | 该状态为每个线程独享。 |
| Group | 该状态为同一个组里面所有线程共享。 |
| Benchmark | 该状态在所有线程间共享。 |
默认值是Scope.Thread。
@Warmup
进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。其中的参数 iterations 也就非常好理解了,就是预热轮数。
为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
也可用在测试方法上。
| 参数 | 说明 |
|---|---|
| iterations | 进行测试的轮次 |
| time | 每轮进行的时长 |
| timeUnit | 时长单位 |
| batchSize | 每个操作的基准方法调用数 |
@Measurement
度量,其实就是一些基本的测试参数。
| 参数 | 说明 |
|---|---|
| iterations | 进行测试的轮次 |
| time | 每轮进行的时长 |
| timeUnit | 时长单位 |
| batchSize | 每个操作的基准方法调用数 |
也可用在测试方法上。
@Threads
每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为 cpu 乘以 2。
@Fork
进行 fork 的次数。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
默认值是5。
@OutputTimeUnit
基准测试结果的时间类型。一般选择秒、毫秒、微秒。
默认值是秒。
@Benchmark
方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。
@Param
属性级注解,@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。
@Setup
方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的。
@TearDown
方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。
正确的微基准测试
如果编写的代码本身就存在着诸多问题,那么即使使用正确的测试方法,也不可能得到正确的测试结果。这些测试代码中的问题应该由我们进行主动避免,那么有哪些常见问题呢?下面介绍两种最常见的情况。
无用代码消除 ( Dead Code Elimination )
也有网友形象的翻译成死代码,死代码是指那些 JVM 经过检查发现的根本不会使用到的代码。
常量折叠 (Constant Folding)
在对 Java 源文件编译的过程中,编译器通过语法分析,可以发现某些能直接得到计算结果而不会再次更改的代码,然后会将计算结果记录下来,这样在执行的过程中就不需要再次运算了。
其实 JVM 做的优化操作远不止上面这些,还有比如常量传播(Constant Propagation)、循环展开(Loop Unwinding)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、本块重排序(Basic Block Reordering)、范围检查消除(Range Check Elimination)等。

浙公网安备 33010602011771号