Java 字符串拼接性能实测:基于 JMH 的微基准测试
在 Java 开发中,字符串拼接操作无处不在。你可能会直接使用 +
,也可能选择 StringBuilder
或 StringBuffer
。它们在性能上究竟有何差别?在循环中拼接多个字符串时,哪种方式更高效?
本文基于 JMH(Java Microbenchmark Harness)进行了系统性测试,并使用 GitHub Actions 在 Ubuntu 环境中实测了不同字符串拼接方式的性能。
🧪 测试目标
比较以下三种拼接方式在高频场景下的性能差异:
+
操作符(语法糖,编译期转为 StringBuilder.append
)
StringBuilder.append
StringBuffer.append
🧰 项目创建与配置(Maven)
1. 创建基础工程
mvn archetype:generate \
-DgroupId=com.xinchentechnote \
-DartifactId=string-jmh \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
复制代码
2. 配置 pom.xml
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
复制代码
📄 测试代码实现
package com.xinchentechnote;
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class StringConcatBenchmark {
private String str1 = "Hello";
private String str2 = "World";
private String str3 = "Java";
private int count = 100;
@Benchmark
public String testStringBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(str1);
sb.append(str2);
sb.append(str3);
}
return sb.toString();
}
@Benchmark
public String testStringBuffer() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < count; i++) {
sb.append(str1);
sb.append(str2);
sb.append(str3);
}
return sb.toString();
}
@Benchmark
public String testStringPlus() {
String result = "";
for (int i = 0; i < count; i++) {
result += str1;
result += str2;
result += str3;
}
return result;
}
}
复制代码
🚀 GitHub Actions 自动化测试配置
name: JMH Benchmarks
on:
workflow_dispatch:
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: '17'
cache: maven
- run: mvn clean install -DskipTests
- run: java -jar target/benchmarks.jar StringConcatBenchmark
复制代码
📊 实测结果(Ubuntu + GitHub Actions)
📈 吞吐量测试(Throughput)
单位:每微秒执行的操作数(ops/us)
⏱ 平均耗时测试(AverageTime)
单位:每次操作的平均耗时(us/op)
🔍 原理解析
✅ 使用建议
🏁 总结
本次 JMH 实测验证了开发经验中的最佳实践:在高频场景中,推荐使用 StringBuilder
进行字符串拼接。
💡 JMH 小知识:Java 微基准测试利器
JMH (Java Microbenchmark Harness) 是由 Oracle 和 OpenJDK 团队专为 Java 编写的微基准测试框架,用于衡量 Java 方法在纳秒到微秒级别的性能表现。JMH 特别适用于需要精细分析方法调用开销、编译优化、副作用等对性能影响的场景。
核心术语与注解解释:
推荐配置参数说明:
@BenchmarkMode({Mode.Throughput, Mode.AverageTime}) // 同时测吞吐量和平均耗时
@OutputTimeUnit(TimeUnit.MICROSECONDS) // 输出微秒为单位
@State(Scope.Thread) // 每个线程独立状态
@Fork(1) // 启动 1 次 JVM
@Warmup(iterations = 5, time = 1) // 预热 5 次,每次 1 秒
@Measurement(iterations = 5, time = 1) // 采样 5 次,每次 1 秒
复制代码
为什么不能简单用 System.currentTimeMillis?
因为 JVM 启动初期 JIT 编译未完成、内联尚未展开、逃逸分析等优化机制尚未介入,初始运行的耗时非常不稳定。而 JMH 通过:
自动 warm-up 预热阶段;
多轮 fork JVM 隔离优化影响;
统计误差和波动范围(Error);
确保了测得结果更真实、更接近应用实际表现。
希望本文能为你在日常开发与性能优化中提供量化参考!
评论