OpenJDK JMH(Java Microbenchmark Harness)是一个专门用于编写、运行和分析Java基准测试的工具库。它由OpenJDK团队开发,帮助开发者精确测量Java代码的性能,特别是在微秒或纳秒级别的操作,常用于性能调优。
前言
在日常 Java 开发工作中,我们会做很多的性能优化。然而,如何准确测量一段代码的性能?如何避免 JVM 的即时编译(JIT)和垃圾回收(GC)对测试结果的干扰?这就是 OpenJDK JMH(Java Microbenchmark Harness) 的核心价值。本文将深入探讨 JMH 的使用方法和适用场景,并通过实际案例帮忙我们快速入门。
一、JMH 是什么?
JMH 是由 OpenJDK 团队开发的 微基准测试框架,专为测量极短代码段(纳秒到微秒级别)的性能而设计。它通过以下机制确保测试的准确性:
- 多次预热(Warmup):消除 JIT 编译(首次解释执行耗费的时间)对测试代码的影响。
- 多轮次测量(Measurement):统计稳定的性能数据。
- 死代码消除(Dead Code Elimination)防御:使用 Blackhole 防止 JVM 优化掉无用代码,精准测试代码性能。
死代码指的是在程序运行过程中永远不会被执行,或者执行后其结果不会被使用的代码,JIT会直接跳过无用计算,优化程序执行。
// 死代码
@Benchmark
public int deadCode() {
int x = 5;
int y = 10;
return x + y; // JIT可能直接返回15,跳过计算
}
二、JMH如何使用?
1.引入依赖
Maven(示例):
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
<scope>test</scope>
</dependency>
2.编写基准测试类
@BenchmarkMode(Mode.AverageTime) // 测试模式:平均时间
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 输出时间单位:纳秒
@Warmup(iterations = 3, time = 1) // 预热:3 轮,每轮 1 秒
@Measurement(iterations = 5, time = 1) // 测量:5 轮,每轮 1 秒
@State(Scope.Thread) // 每个线程独享状态
public class StringBenchmark {
private String str1 = "Hello";
private String str2 = "World";
@Benchmark
public String stringConcat() {
return str1 + str2; // 测试字符串拼接
}
@Benchmark
public String stringBuilder() {
return new StringBuilder().append(str1).append(str2).toString(); // 测试StringBuilder
}
public static void main(String[] args) throws Exception {
Options opts = new OptionsBuilder()
.include(StringBenchmark.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opts).run();
}
}
2.运行测试&结果分析
Benchmark Mode Cnt Score Error Units
StringBenchmark.stringBuilder avgt 25 8.972 ± 0.963 ns/op
StringBenchmark.stringConcat avgt 25 9.170 ± 0.692 ns/op
- Score:平均每次操作耗时。
- Error:误差范围。
- 结论:StringBuilder 的性能是优于 + 拼接。
三、JMH适用场景
- 微优化验证:验证算法或数据结构的性能差异(如 ArrayList vs LinkedList)。 示例:对比不同 JSON 序列化库(Jackson vs Gson)的性能。
- JVM 特性研究:分析 JIT 编译优化效果(如方法内联、循环展开)。 研究 GC 策略对代码性能的影响。
- 框架或库的性能调优:在开发高性能库(如 Netty、HikariCP)时,验证关键路径的性能。
- 并发代码测试:测量锁机制(synchronized vs ReentrantLock)的性能差异。 验证无锁数据结构(如 AtomicInteger)的效率。
四、实际案例
HashMap 的初始容量优化
案例背景: 默认 new HashMap() 的初始容量为 16,当数据量较大时频繁扩容会降低性能。通过 JMH 验证设置初始容量的优化效果。 基准测试代码
@State(Scope.Thread)
public class HashMapBenchmark {
@Param({"100", "1000", "10000"})
private int size;
@Benchmark
public Map<Integer, String> defaultHashMap() {
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < size; i++) {
map.put(i, "Value" + i);
}
return map;
}
@Benchmark
public Map<Integer, String> optimizedHashMap() {
Map<Integer, String> map = new HashMap<>(size * 2); // 避免扩容
for (int i = 0; i < size; i++) {
map.put(i, "Value" + i);
}
return map;
}
}
测试结果
Benchmark (size) Mode Cnt Score Error Units
StringBenchmark.defaultHashMap 100 avgt 25 1194.284 ± 56.767 ns/op
StringBenchmark.defaultHashMap 1000 avgt 25 12921.448 ± 572.845 ns/op
StringBenchmark.defaultHashMap 10000 avgt 25 129174.553 ± 5810.533 ns/op
StringBenchmark.optimizedHashMap 100 avgt 25 952.843 ± 42.588 ns/op
StringBenchmark.optimizedHashMap 1000 avgt 25 10746.718 ± 461.667 ns/op
StringBenchmark.optimizedHashMap 10000 avgt 25 109973.889 ± 2229.168 ns/op
结论:预先设置初始容量的 HashMap 性能显著提升,尤其在数据量较大时。
synchronized vs ReentrantLock 性能对比
案例背景: synchronized:JVM 内置关键字,自动加锁/释放锁。 ReentrantLock:JDK 提供的显式锁,支持公平锁、非公平锁、条件变量等高级功能。 此案例将比较两种锁机制在高竞争场景下的性能差异。
基准测试代码
@State(Scope.Group) // 使用Group隔离不同线程组
@BenchmarkMode(Mode.Throughput) // 测试吞吐量(ops/ms)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class LockBenchmark {
private int counter;
private final Object syncLock = new Object();
private final ReentrantLock reentrantLock = new ReentrantLock();
@Benchmark
@Group("synchronized")
@GroupThreads(30) // 模拟30个竞争线程
public void synchronizedIncrement(Blackhole bh) {
synchronized (syncLock) {
counter++;
bh.consume(counter); // 防止死代码消除
}
}
@Benchmark
@Group("reentrantLock")
@GroupThreads(30)
public void reentrantLockIncrement(Blackhole bh) {
reentrantLock.lock();
try {
counter++;
bh.consume(counter);
} finally {
reentrantLock.unlock();
}
}
测试结果
高竞争场景(30线程)
Benchmark Mode Cnt Score Error Units
StringBenchmark.reentrantLock thrpt 25 51273.021 ± 3051.450 ops/ms
StringBenchmark.synchronized thrpt 25 21527.434 ± 1466.118 ops/ms
结论:在高竞争下,ReentrantLock 吞吐量比 synchronized 高
- ReentrantLock 性能更好,因为: 1、使用 CAS(Compare-And-Swap)减少线程阻塞。 2、支持非公平锁(默认),减少上下文切换。
- synchronized 在 JDK 1.6 后已优化(锁升级机制),但仍有开销。
五、JMH 的最佳实践
- 避免在测试方法中留下无用代码:使用 Blackhole.consume() 防止 JVM 优化掉计算结果。
Blackhole 的作用是“吞噬”计算结果,让 JVM 无法判断计算结果是否被使用,从而避免死代码消除。你可以将计算结果传递给 Blackhole,JMH 会确保这些计算不会被优化掉。 @Benchmark public void test(Blackhole bh) { int result = expensiveCalculation(); bh.consume(result); // 防止结果被优化 }
- 合理设置预热和测量参数:对于 JIT 编译复杂的方法,有效的增加预热次数可以提高测试精度(@Warmup(iterations = 5))。
- 使用 @Param 参数化测试:测试不同场景下的性能表现。
总结
JMH 是 Java 开发者进行性能调优的有效工具,它能精确测量代码段微秒/纳秒下的性能,帮助开发者做出数据驱动的优化决策。 JMH官方案例 JMH参数文档