项目概况

JMH基准测试

2026-01-294 min read后端技术
JMH

参考链接

官方文档

性能调优必备利器之 JMH

一、JMH

JMH即Java Microbenchmark Harness,是Java用来做基准测试的一个工具,该工具由OpenJDK提供并维护,测试结果可信度高。

基准测试Benchmark是测量、评估软件性能指标的一种测试,对某个特定目标场景的某项性能指标进行定量的和可对比的测试。

1.1 为什么要用JMH

if 快还是 switch 快?HashMap 的初始化 size 要不要指定,指定之后性能可以提高多少?各种序列化方法哪个耗时更短?

无论出自何种原因需要进行性能评估,量化指标总是必要的。

在大部分场合,简单地回答谁快谁慢是远远不够的,如何将程序性能量化呢?

这就需要我们的主角 JMH 登场了!

二、前期准备

2.1 引入依赖

xml
1<dependencies> 2 <dependency> 3 <groupId>org.openjdk.jmh</groupId> 4 <artifactId>jmh-core</artifactId> 5 <version>1.23</version> 6 </dependency> 7 8 <dependency> 9 <groupId>org.openjdk.jmh</groupId> 10 <artifactId>jmh-generator-annprocess</artifactId> 11 <version>1.23</version> 12 </dependency> 13</dependencies>

2.2 安装插件

jmh-java-microbenchmark-harness

当安装插件后,只要被@Benchmark就会认为是一个test方法,出现执行按钮

当然也可以不安装插件,如下代码也会进行输出

java
1 public static void main(String[] args) throws Exception { 2 Options opt = new OptionsBuilder() 3 .include(BenchmarkTest.class.getSimpleName()) 4 .resultFormat(ResultFormatType.JSON) 5 .build(); 6new Runner(opt).run(); 7}

三、使用说明

在正式使用前,先介绍下JMH基准测试中可以使用的注解。

3.1 @BenchmarkMode

用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。

  1. Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
  2. AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
  3. SampleTime:随机取样,最后输出取样结果的分布
  4. SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
  5. All:上面的所有模式都执行一次

3.2 @State

通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下

  1. Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
  2. Scope.Group:同一个线程在同一个 group 里共享实例
  3. Scope.Thread:默认的 State,每个测试线程分配一个实例

3.3 @OutputTimeUnit

为统计结果的时间单位,可用于类或者方法注解

3.4 @Warmup

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

  1. iterations:预热的次数
  2. time:每次预热的时间
  3. timeUnit:时间的单位,默认秒
  4. batchSize:批处理大小,每次操作调用几次方法

为什么需要预热?

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

3.5 @Measurement

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

  1. iterations:执行的次数
  2. time:每次执行时间
  3. timeUnit:时间的单位,默认秒
  4. batchSize:批处理大小,每次操作调用几次方法

3.6 @Threads

配置同时起多少个线程执行,默认值世 Runtime.getRuntime().availableProcessors()

3.7 @Fork

进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。

3.8 @Param

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

四、场景案例

4.1 for循环性能

循环100w次,并输出。预热2次,执行2次,2个线程,1个进程,统计结果单位毫秒。

方法平均耗时
普通for循环7898ms
增强for循环8078ms
迭代器8314ms
Stream循环9085ms
java
1 2import org.openjdk.jmh.annotations.*; 3import org.openjdk.jmh.results.format.ResultFormatType; 4import org.openjdk.jmh.runner.Runner; 5import org.openjdk.jmh.runner.options.Options; 6import org.openjdk.jmh.runner.options.OptionsBuilder; 7 8import java.util.ArrayList; 9import java.util.Iterator; 10import java.util.List; 11import java.util.concurrent.TimeUnit; 12 13@BenchmarkMode(Mode.AverageTime) 14@OutputTimeUnit(TimeUnit.MILLISECONDS) 15@Warmup(iterations = 2) 16@Measurement(iterations = 2) 17@Threads(2) 18@Fork(1) 19@State(Scope.Thread) 20public class ForBenchmarkTest { 21 22 private static List<Integer> integers; 23 24static { 25 integers = generate(1000000); 26} 27 28 private static List<Integer> generate(Integer max) {2 29 List<Integer> result = new ArrayList<>(max); 30for (int i = 0; i < max; i++) { 31 result.add(i); 32} 33 return result; 34} 35 36 @Benchmark 37 public void forTest() { 38 for (int i = 0; i < integers.size(); i++) { 39 System.out.println(integers.get(i)); 40} 41 } 42 43 @Benchmark 44 public void forEachTest() { 45 for (Integer integer : integers) { 46 System.out.println(integer); 47} 48 } 49 50 @Benchmark 51 public void iteratorTest() { 52 Iterator<Integer> iterator = integers.iterator(); 53while (iterator.hasNext()) { 54 System.out.println(iterator.next()); 55} 56 } 57 58 @Benchmark 59 public void streamForEachTest() { 60 integers.forEach(System.out::println); 61} 62 63 public static void main(String[] args) throws Exception { 64 Options opt = new OptionsBuilder() 65 .include(ForBenchmarkTest.class.getSimpleName()) 66 .resultFormat(ResultFormatType.JSON) 67 .build(); 68new Runner(opt).run(); 69} 70} 71

4.2 反射性能

通过上面数据可以得到。反射缺失很耗时。开启安全检查比不开启耗时, 如果反射在加上缓存,会比较好一点。

  • metaClass1k 是1k次操作,反射+缓存
  • metaClass1w 是1w次操作,反射+缓存
  • test1k 是1k次正常操作
  • test1w 是1w次正常操作
  • testReflection1k 是1k次操作,反射
  • testReflection1w 是1w次操作,反射
  • testReflectionAccessible1k 是1k次操作,反射,关闭安全检查
  • testReflectionAccessible1k 是1w次操作,反射,关闭安全检查
java
1@BenchmarkMode(Mode.AverageTime) 2@OutputTimeUnit(TimeUnit.SECONDS) 3@Warmup(iterations = 5, time = 1) 4@Measurement(iterations = 10, time = 1) 5@Threads(2) 6@Fork(1) 7@State(Scope.Thread) 8public class ReflectionBenchmarkTest { 9 10 private static class Mode { 11 private Integer age; 12 13public void setAge(Integer age) { 14 this.age = age; 15} 16 } 17 18 @Benchmark 19 public void test1k() { 20 Mode mode = new Mode(); 21Loops.loop(1000, mode::setAge); 22} 23 24 @Benchmark 25 public void testReflection1k() { 26 Mode mode = new Mode(); 27Loops.loop(1000, i -> { 28 Method setAge = null; 29 try { 30 setAge = mode.getClass().getMethod("setAge", Integer.class); 31 setAge.invoke(mode, i); 32 } catch (Throwable e) { 33 throw new RuntimeException(e); 34 } 35 }); 36} 37 38 @Benchmark 39 public void testReflectionAccessible1k() { 40 Mode mode = new Mode(); 41Loops.loop(1000, i -> { 42 Method setAge = null; 43 try { 44 setAge = mode.getClass().getMethod("setAge", Integer.class); 45 setAge.setAccessible(true); 46 setAge.invoke(mode, i); 47 } catch (Throwable e) { 48 throw new RuntimeException(e); 49 } 50 }); 51} 52 53 @Benchmark 54 public void test1w() { 55 Mode mode = new Mode(); 56Loops.loop(10000, mode::setAge); 57} 58 59 @Benchmark 60 public void testReflection1w() { 61 Mode mode = new Mode(); 62Loops.loop(10000, i -> { 63 Method setAge = null; 64 try { 65 setAge = mode.getClass().getMethod("setAge", Integer.class); 66 setAge.invoke(mode, i); 67 } catch (Throwable e) { 68 throw new RuntimeException(e); 69 } 70 }); 71} 72 73 @Benchmark 74 public void testReflectionAccessible1w() { 75 Mode mode = new Mode(); 76Loops.loop(10000, i -> { 77 Method setAge = null; 78 try { 79 setAge = mode.getClass().getMethod("setAge", Integer.class); 80 setAge.setAccessible(true); 81 setAge.invoke(mode, i); 82 } catch (Throwable e) { 83 throw new RuntimeException(e); 84 } 85 }); 86} 87 88 @Benchmark 89 public void metaClass1k() { 90 MetaClass metaClass = MetaClass.forClass(Mode.class, new DefaultReflectorFactory()); 91Mode mode = new Mode(); 92Loops.loop(10000, i -> { 93 try { 94 Invoker setAge = metaClass.getSetInvoker("age"); 95 setAge.invoke(mode, new Object[]{i}); 96 } catch (Throwable e) { 97 throw new RuntimeException(e); 98 } 99 }); 100} 101 102 @Benchmark 103 public void metaClass1w() { 104 MetaClass metaClass = MetaClass.forClass(Mode.class, new DefaultReflectorFactory()); 105Mode mode = new Mode(); 106Loops.loop(10000, i -> { 107 try { 108 Invoker setAge = metaClass.getSetInvoker("age"); 109 setAge.invoke(mode, new Object[]{i}); 110 } catch (Throwable e) { 111 throw new RuntimeException(e); 112 } 113 }); 114} 115 116 117 public static void main(String[] args) throws Exception { 118 Options opt = new OptionsBuilder() 119 .include(ReflectionBenchmarkTest.class.getSimpleName()) 120 .resultFormat(ResultFormatType.JSON) 121 .build(); 122new Runner(opt).run(); 123} 124} 125