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);
确保了测得结果更真实、更接近应用实际表现。
希望本文能为你在日常开发与性能优化中提供量化参考!
评论