JMH案例

wen java案例 3

本文目录导读:

JMH案例

  1. 案例目标
  2. 环境准备 (Maven依赖)
  3. 基准测试代码
  4. 运行方式
  5. 输出结果解读
  6. 进阶技巧:Blackhole 与死代码消除
  7. 进阶技巧:多线程测试
  8. 常见陷阱与规避
  9. 完整配置文件示例 (JSON格式)
  10. JMH最佳实践清单

这是一份关于 JMH(Java Microbenchmark Harness) 的完整实战案例解析,我将带你从环境搭建、编写基准测试到结果解读,逐步完成一个典型的性能测试案例。


案例目标

比较 StringBuilderStringBufferString.concat() 在字符串拼接时的性能差异,并测试不同初始容量对 StringBuilder 的影响。


环境准备 (Maven依赖)

<dependencies>
    <!-- JMH 核心库 -->
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.37</version>
    </dependency>
    <!-- JMH 注解处理 -->
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.37</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

基准测试代码

package com.example.jmh;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
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.concurrent.TimeUnit;
@BenchmarkMode(Mode.Throughput)          // 吞吐量模式(每秒执行次数)
@OutputTimeUnit(TimeUnit.MILLISECONDS)   // 输出时间单位
@State(Scope.Thread)                     // 每个线程独立实例
@Warmup(iterations = 5, time = 1)        // 预热 5 轮,每轮 1 秒
@Measurement(iterations = 5, time = 1)   // 正式测量 5 轮,每轮 1 秒
@Fork(2)                                 // 2个JVM进程
public class StringConcatBenchmark {
    // ---- 输入数据 ----
    @Param({"10", "100", "1000"})         // 测试不同长度的字符串
    private int length;
    private String[] fragments;
    @Setup
    public void setup() {
        // 预生成随机的字符串片段
        fragments = new String[length];
        for (int i = 0; i < length; i++) {
            fragments[i] = "str-" + i + "-";
        }
    }
    // ---- 测试1: String.concat() ----
    @Benchmark
    public String testStringConcat() {
        String result = "";
        for (String s : fragments) {
            result = result.concat(s);
        }
        return result;
    }
    // ---- 测试2: StringBuilder(默认容量16) ----
    @Benchmark
    public String testStringBuilder() {
        StringBuilder sb = new StringBuilder();
        for (String s : fragments) {
            sb.append(s);
        }
        return sb.toString();
    }
    // ---- 测试3: StringBuilder(预分配容量) ----
    @Benchmark
    public String testStringBuilderWithCapacity() {
        // 预估算容量: 每段字符串平均长度 * 段数
        int estimated = length * 8; 
        StringBuilder sb = new StringBuilder(estimated);
        for (String s : fragments) {
            sb.append(s);
        }
        return sb.toString();
    }
    // ---- 测试4: StringBuffer ----
    @Benchmark
    public String testStringBuffer() {
        StringBuffer sb = new StringBuffer();
        for (String s : fragments) {
            sb.append(s);
        }
        return sb.toString();
    }
    // ---- 主函数: 启动JMH基准测试 ----
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(StringConcatBenchmark.class.getSimpleName())
                .forks(2)
                .build();
        new Runner(opt).run();
    }
}

运行方式

方式A: 主方法运行 (推荐)

直接运行 main 方法,JMH会自动进行所有测试。

方式B: 命令行运行 (Maven)

mvn clean package
java -jar target/benchmarks.jar

输出结果解读

运行后,控制台会输出 每个Benchmark方法的详细性能报告,以下是典型的输出(节选):

Benchmark                                          (length)  Mode  Cnt     Score    Error   Units
StringConcatBenchmark.testStringBuffer                 10  thrpt   10   215.367 ±  5.892  ops/ms
StringConcatBenchmark.testStringBuffer                100  thrpt   10    78.923 ±  2.105  ops/ms
StringConcatBenchmark.testStringBuffer               1000  thrpt   10    18.452 ±  0.887  ops/ms
StringConcatBenchmark.testStringBuilder                10  thrpt   10   280.441 ±  9.124  ops/ms
StringConcatBenchmark.testStringBuilder               100  thrpt   10   110.235 ±  3.456  ops/ms
StringConcatBenchmark.testStringBuilder              1000  thrpt   10    23.889 ±  1.012  ops/ms
StringConcatBenchmark.testStringBuilderWithCapacity    10  thrpt   10   295.112 ± 11.008  ops/ms
StringConcatBenchmark.testStringBuilderWithCapacity   100  thrpt   10   125.678 ±  4.229  ops/ms
StringConcatBenchmark.testStringBuilderWithCapacity  1000  thrpt   10    28.334 ±  1.567  ops/ms
StringConcatBenchmark.testStringConcat                 10  thrpt   10    45.238 ±  2.334  ops/ms
StringConcatBenchmark.testStringConcat                100  thrpt   10    12.678 ±  0.567  ops/ms
StringConcatBenchmark.testStringConcat               1000  thrpt   10     3.452 ±  0.123  ops/ms

结果分析:

方法 10段 100段 1000段
String.concat 45 ops/ms 12 ops/ms 4 ops/ms 最慢,性能随长度急剧下降
StringBuffer 215 ops/ms 78 ops/ms 18 ops/ms 次之,有同步开销
StringBuilder 280 ops/ms 110 ops/ms 24 ops/ms 快,无同步开销
StringBuilder+Capacity 295 ops/ms 125 ops/ms 28 ops/ms 最快,预分配容量减少了扩容

进阶技巧:Blackhole 与死代码消除

在上面的例子中,如果不返回结果,但为了防止JIT认为结果未被使用而优化掉,可以使用 Blackhole

@Benchmark
public void testAppendWithBlackhole(Blackhole bh) {
    StringBuilder sb = new StringBuilder();
    for (String s : fragments) {
        sb.append(s);
    }
    // 将结果传递给 Blackhole,避免死代码消除
    bh.consume(sb.toString());
}

进阶技巧:多线程测试

@Benchmark
@Threads(4)  // 4个线程并发执行
public String testStringBuilderThreaded() {
    StringBuilder sb = new StringBuilder();
    for (String s : fragments) {
        sb.append(s);
    }
    return sb.toString();
}

常见陷阱与规避

陷阱1: 循环展开 (Loop Hoisting)

@Benchmark
public void testWrong() {
    int x = 1;
    for (int i = 0; i < 1000; i++) {
        x = x + i;  // JIT 可能会将整个循环优化掉
    }
}
// 修正: 通过 Blackhole 消费结果
@Benchmark
public void testCorrect(Blackhole bh) {
    int x = 1;
    for (int i = 0; i < 1000; i++) {
        x = x + i;
    }
    bh.consume(x);
}

陷阱2: 常数折叠 (Constant Folding)

// 不推荐: 直接写死参数
@Benchmark
public int testConst() {
    return Math.sqrt(2);   // JIT 会提前计算出结果
}
// 推荐: 使用 @Param 注入
@Benchmark
public int testParam(@Param("2") double x) {
    return (int) Math.sqrt(x);
}

陷阱3: 误用状态 (State)

  • @State(Scope.Thread): 每个线程单独实例,适合无竞争的场景。
  • @State(Scope.Benchmark): 所有线程共享同一个实例,测试并发竞争
  • @State(Scope.Group): 用于分组测试(@Group 注解)。

完整配置文件示例 (JSON格式)

还可以通过 -prof 参数启用分析器:

java -jar target/benchmarks.jar -prof stack -prof gc -prof perf

这样会额外输出 堆栈采样GC次数/时间CPU硬件计数器,帮助你做更深入的分析。


JMH最佳实践清单

  1. 每个@Benchmark方法只测一件事,不要混入无关逻辑。
  2. 使用@Setup初始化数据,避免在循环中执行IO、网络等操作。
  3. 总是使用Blackhole消耗结果
  4. 适当预热 (至少3-5轮) 让JIT充分编译。
  5. 使用多个@Fork 避免测试间相互影响。
  6. @Param控制变量,而不要硬编码。

现在你可以复制上述代码,直接运行 main 方法查看结果,如果遇到任何报错或需要更详细的解释,欢迎随时追问!

抱歉,评论功能暂时关闭!