本文目录导读:

这是一份关于 JMH(Java Microbenchmark Harness) 的完整实战案例解析,我将带你从环境搭建、编写基准测试到结果解读,逐步完成一个典型的性能测试案例。
案例目标
比较 StringBuilder、StringBuffer 和 String.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最佳实践清单
- 每个
@Benchmark方法只测一件事,不要混入无关逻辑。 - 使用
@Setup初始化数据,避免在循环中执行IO、网络等操作。 - 总是使用
Blackhole消耗结果。 - 适当预热 (至少3-5轮) 让JIT充分编译。
- 使用多个
@Fork避免测试间相互影响。 - 用
@Param控制变量,而不要硬编码。
现在你可以复制上述代码,直接运行 main 方法查看结果,如果遇到任何报错或需要更详细的解释,欢迎随时追问!