📦 依赖引入
Maven 依赖 (pom.xml)
<!-- JMH Core:核心库 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
<scope>test</scope>
</dependency>
<!-- JMH 注解处理器:编译时生成基准测试代码 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
<scope>test</scope>
</dependency>
<!-- Lombok:简化日志代码(可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
版本说明
- JMH 版本: 1.36(2023年发布,支持 Java 17+)
- 建议 JDK: Java 11+ (推荐 Java 17)
- Scope:
test(仅在测试阶段使用)
💻 代码 Demo
完整示例代码
package com.example.neo4jdemo.benchmark;
import lombok.extern.slf4j.Slf4j;
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.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread) // 每个线程一个实例
@BenchmarkMode({Mode.Throughput, Mode.AverageTime}) // 测试吞吐量和平均时间
@OutputTimeUnit(TimeUnit.SECONDS) // 输出单位:秒
@Fork(value = 1) // 启动 1 个 JVM 进程
@Warmup(iterations = 2, time = 1) // 预热:2 次迭代,每次 1 秒
@Measurement(iterations = 3, time = 2) // 测量:3 次迭代,每次 2 秒
@Slf4j
public class SimpleBenchmark {
private List<Integer> numbers;
private static final int DATA_SIZE = 100000000;
/**
* Setup 阶段:初始化测试数据
*/
@Setup(Level.Trial)
public void setUp() {
log.info("========== Setup 开始 ==========");
numbers = new ArrayList<>();
for (int i = 0; i < DATA_SIZE; i++) {
numbers.add(i);
}
log.info("✓ 初始化了 {} 个数据", DATA_SIZE);
}
/**
* TearDown 阶段:清理资源
*/
@TearDown(Level.Trial)
public void tearDown() {
log.info("✓ 清理资源完成");
}
/**
* 场景 1: 传统 for-each 循环
*/
@Benchmark
public long scenario1_SimpleLoop() {
long sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
/**
* 场景 2: Stream API
*/
@Benchmark
public long scenario2_StreamAPI() {
return numbers.stream()
.mapToLong(Integer::longValue)
.sum();
}
/**
* 场景 3: 并行 Stream
*/
@Benchmark
public long scenario3_ParallelStream() {
return numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
}
/**
* 主方法:运行基准测试
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SimpleBenchmark.class.getSimpleName())
.forks(1)
.warmupIterations(2)
.measurementIterations(3)
.build();
new Runner(opt).run();
}
}
运行方式
方式 1: 直接运行 main 方法
# IDE 中直接运行 main 方法
方式 2: Maven 命令
mvn clean test -Dtest=SimpleBenchmark
方式 3: 打包后运行(推荐生产环境)
mvn clean package
java -jar target/benchmarks.jar
🔬 工作原理
JMH 执行流程
1. Compilation Phase(编译阶段)
├─ 注解处理器扫描 @Benchmark 注解
└─ 生成辅助代码到 target/generated-test-sources/
2. Warmup Phase(预热阶段)
├─ 目的:让 JIT 编译器优化代码
├─ 执行 @Warmup 指定的迭代次数
└─ 结果不计入最终统计
3. Measurement Phase(测量阶段)
├─ 执行 @Measurement 指定的迭代次数
├─ 精确测量每次迭代的时间
└─ 统计平均值、标准差等指标
4. Result Phase(结果输出)
├─ 汇总所有 Fork 的结果
├─ 计算统计指标(均值、误差、置信区间)
└─ 输出可读性报告
内部机制
- 代码隔离: 每个 Benchmark 在独立的 JVM 进程中运行(Fork)
- JIT 优化: 通过预热让 JIT 充分优化,避免影响测量
- 死码消除防护: JMH 会确保测试代码不被编译器优化掉
- 统计学方法: 多次迭代取平均,计算标准差和置信区间
🏷️ 主要注解详解
类级别注解
| 注解 | 说明 | 常用参数 |
|---|---|---|
@State | 定义测试状态的生命周期 | Scope.Thread(线程隔离)Scope.Benchmark(全局共享)Scope.Group(组内共享) |
@BenchmarkMode | 指定测试模式 | Mode.Throughput(吞吐量)Mode.AverageTime(平均时间)Mode.SampleTime(采样时间)Mode.SingleShotTime(单次时间) |
@OutputTimeUnit | 结果输出的时间单位 | TimeUnit.NANOSECONDSTimeUnit.MICROSECONDSTimeUnit.MILLISECONDSTimeUnit.SECONDS |
@Fork | 指定启动几个 JVM 进程 | value = 1(1个进程)jvmArgs = {"-Xms2g"}(JVM参数) |
@Warmup | 配置预热阶段 | iterations = 2(迭代次数)time = 1(每次持续时间) |
@Measurement | 配置测量阶段 | iterations = 3(迭代次数)time = 2(每次持续时间) |
@Threads | 指定并发线程数 | value = 4(4个线程) |
方法级别注解
| 注解 | 说明 | Level 参数 |
|---|---|---|
@Benchmark | 标记性能测试方法 | - |
@Setup | 初始化方法(在测试前执行) | Level.Trial(整个测试前)Level.Iteration(每次迭代前)Level.Invocation(每次调用前) |
@TearDown | 清理方法(在测试后执行) | 同 @Setup |
@Param | 参数化测试 | @Param({"100", "1000", "10000"}) |
注解示例对比
// 示例 1:最小配置
@Benchmark
public void testMethod() {
// 测试代码
}
// 示例 2:完整配置
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(2)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
public class MyBenchmark {
@Param({"100", "1000"})
private int size;
@Setup(Level.Trial)
public void init() { }
@Benchmark
public void test() { }
@TearDown(Level.Trial)
public void cleanup() { }
}
⚠️ 注意事项
1. 避免常见陷阱
死码消除(Dead Code Elimination)
// ❌ 错误:返回值未使用,可能被 JIT 优化掉
@Benchmark
public void badBenchmark() {
list.stream().count(); // 结果未使用
}
// ✅ 正确:返回结果或使用 Blackhole
@Benchmark
public long goodBenchmark1() {
return list.stream().count();
}
@Benchmark
public void goodBenchmark2(Blackhole blackhole) {
blackhole.consume(list.stream().count());
}
循环优化(Loop Unrolling)
// ❌ 错误:循环在基准方法内,可能被优化
@Benchmark
public void badLoop() {
for (int i = 0; i < 1000; i++) {
doSomething();
}
}
// ✅ 正确:让 JMH 框架控制迭代
@Benchmark
public void goodLoop() {
doSomething();
}
2. 预热的重要性
// 预热不足会导致测量结果包含 JIT 编译时间
@Warmup(iterations = 1, time = 1) // ❌ 太少
@Warmup(iterations = 5, time = 2) // ✅ 推荐
原因: JVM 的 JIT 编译器需要多次执行才会将热点代码编译为机器码。
3. Fork 的必要性
@Fork(0) // ❌ 不推荐:在当前 JVM 运行,可能受其他因素干扰
@Fork(1) // ✅ 推荐:独立 JVM,结果更可靠
@Fork(3) // ✅ 更好:多次运行取平均,降低误差
4. 状态管理
@State(Scope.Thread)
public class MyBenchmark {
private List<String> data; // 线程独立
@Setup(Level.Trial) // ✅ Trial:整个测试初始化一次
public void setUp() {
data = new ArrayList<>();
}
@Setup(Level.Iteration) // ⚠️ Iteration:每次迭代都初始化(性能开销)
public void setUpIteration() { }
}
5. 测量模式选择
| 模式 | 适用场景 | 示例 |
|---|---|---|
Throughput | 高频调用的方法 | 集合遍历、数据处理 |
AverageTime | 关注平均响应时间 | API 请求、数据库查询 |
SampleTime | 查看时间分布 | 找出延迟异常值 |
SingleShotTime | 冷启动性能 | 应用启动、首次加载 |
6. 数据大小
private static final int DATA_SIZE = 100; // ❌ 太小:测量误差大
private static final int DATA_SIZE = 100000000; // ✅ 合适:足够测量差异
7. 结果解读
Benchmark Mode Cnt Score Error Units
SimpleBenchmark.scenario1 thrpt 3 15.234 ± 0.456 ops/s
^^^ ^ ^^^ ^^^
模式 次数 平均值 误差范围
- Score: 越大越好(Throughput 模式)或越小越好(AverageTime 模式)
- Error: 误差范围,越小说明结果越稳定
- 单位: 根据
@OutputTimeUnit和@BenchmarkMode决定
🔗 与 JMH 的关系
SimpleBenchmark 是 JMH 的应用示例
┌─────────────────────────────────────────┐
│ JMH Framework │
│ (Java Microbenchmark Harness) │
│ │
│ ┌─────────────────────────────────┐ │
│ │ JMH Core(核心框架) │ │
│ │ - 基准测试引擎 │ │
│ │ - 统计分析 │ │
│ │ - 结果输出 │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ JMH Annotations(注解系统) │ │
│ │ - @Benchmark │ │
│ │ - @State, @Setup, @TearDown │ │
│ │ - @Fork, @Warmup, @Measurement │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ JMH Annotation Processor │ │
│ │ - 编译时代码生成 │ │
│ │ - 生成辅助测试类 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
↓
┌──────────────────────┐
│ SimpleBenchmark │
│ (你的测试代码) │
│ │
│ 使用 JMH 的注解和 │
│ API 来实现性能测试 │
└──────────────────────┘
JMH 的特点
- 官方支持: 由 OpenJDK 团队维护,与 JVM 深度集成
- 科学严谨: 避免常见的微基准测试陷阱
- 功能强大: 支持多种测量模式、统计分析
- 易于使用: 注解驱动,代码简洁
SimpleBenchmark 的定位
- 入门示例: 演示 JMH 的基本用法
- 最佳实践: 展示正确的测试写法
- 可扩展: 可以作为模板扩展到实际项目
📊 典型输出示例
# JMH version: 1.36
# VM version: JDK 17.0.5, Java HotSpot(TM) 64-Bit Server VM
# Warmup: 2 iterations, 1 s each
# Measurement: 3 iterations, 2 s each
# Timeout: 10 min per iteration
# Threads: 1 thread
# Benchmark mode: Throughput, ops/time & Average time, time/op
Benchmark Mode Cnt Score Error Units
SimpleBenchmark.scenario1_SimpleLoop thrpt 3 15.234 ± 0.456 ops/s
SimpleBenchmark.scenario2_StreamAPI thrpt 3 10.123 ± 0.321 ops/s
SimpleBenchmark.scenario3_ParallelStream thrpt 3 25.678 ± 0.789 ops/s
SimpleBenchmark.scenario1_SimpleLoop avgt 3 0.066 ± 0.002 s/op
SimpleBenchmark.scenario2_StreamAPI avgt 3 0.099 ± 0.003 s/op
SimpleBenchmark.scenario3_ParallelStream avgt 3 0.039 ± 0.001 s/op
结论: 在大数据量下,并行 Stream 性能最好,传统循环次之,串行 Stream 最慢。
🎯 最佳实践总结
- ✅ 始终使用 @Fork:确保测试环境隔离
- ✅ 充分预热:至少 5 次迭代,每次 1-2 秒
- ✅ 足够的测量次数:至少 10 次迭代
- ✅ 返回结果或使用 Blackhole:避免死码消除
- ✅ 合理的数据规模:足够大以观察性能差异
- ✅ 隔离环境:关闭其他应用,避免干扰
- ✅ 多次运行验证:确保结果可重复
- ✅ 记录环境信息:JDK 版本、硬件配置等
📚 参考资源
🚀 进阶方向
学习完 SimpleBenchmark 后,可以探索:
- 参数化测试: 使用
@Param测试不同输入规模 - 状态管理: 理解
Scope.ThreadvsScope.Benchmark - 并发测试: 使用
@Threads测试多线程场景 - 分组测试: 使用
@Group测试协作场景 - 异步测试: 使用
@AsyncBenchmark测试异步代码 - 集成真实场景: 测试数据库、缓存、网络 IO 等