#2 benchmark

30 阅读7分钟

📦 依赖引入

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 的结果
   ├─ 计算统计指标(均值、误差、置信区间)
   └─ 输出可读性报告

内部机制

  1. 代码隔离: 每个 Benchmark 在独立的 JVM 进程中运行(Fork)
  2. JIT 优化: 通过预热让 JIT 充分优化,避免影响测量
  3. 死码消除防护: JMH 会确保测试代码不被编译器优化掉
  4. 统计学方法: 多次迭代取平均,计算标准差和置信区间

🏷️ 主要注解详解

类级别注解

注解说明常用参数
@State定义测试状态的生命周期Scope.Thread(线程隔离)
Scope.Benchmark(全局共享)
Scope.Group(组内共享)
@BenchmarkMode指定测试模式Mode.Throughput(吞吐量)
Mode.AverageTime(平均时间)
Mode.SampleTime(采样时间)
Mode.SingleShotTime(单次时间)
@OutputTimeUnit结果输出的时间单位TimeUnit.NANOSECONDS
TimeUnit.MICROSECONDS
TimeUnit.MILLISECONDS
TimeUnit.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 的特点

  1. 官方支持: 由 OpenJDK 团队维护,与 JVM 深度集成
  2. 科学严谨: 避免常见的微基准测试陷阱
  3. 功能强大: 支持多种测量模式、统计分析
  4. 易于使用: 注解驱动,代码简洁

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 最慢。


🎯 最佳实践总结

  1. 始终使用 @Fork:确保测试环境隔离
  2. 充分预热:至少 5 次迭代,每次 1-2 秒
  3. 足够的测量次数:至少 10 次迭代
  4. 返回结果或使用 Blackhole:避免死码消除
  5. 合理的数据规模:足够大以观察性能差异
  6. 隔离环境:关闭其他应用,避免干扰
  7. 多次运行验证:确保结果可重复
  8. 记录环境信息:JDK 版本、硬件配置等

📚 参考资源


🚀 进阶方向

学习完 SimpleBenchmark 后,可以探索:

  1. 参数化测试: 使用 @Param 测试不同输入规模
  2. 状态管理: 理解 Scope.Thread vs Scope.Benchmark
  3. 并发测试: 使用 @Threads 测试多线程场景
  4. 分组测试: 使用 @Group 测试协作场景
  5. 异步测试: 使用 @AsyncBenchmark 测试异步代码
  6. 集成真实场景: 测试数据库、缓存、网络 IO 等