Java 字符串拼接性能实测:基于 JMH 的微基准测试

Java 字符串拼接性能实测:基于 JMH 的微基准测试

在 Java 开发中,字符串拼接操作无处不在。你可能会直接使用 +,也可能选择 StringBuilderStringBuffer。它们在性能上究竟有何差别?在循环中拼接多个字符串时,哪种方式更高效?

本文基于 JMH(Java Microbenchmark Harness)进行了系统性测试,并使用 GitHub Actions 在 Ubuntu 环境中实测了不同字符串拼接方式的性能。


🧪 测试目标

比较以下三种拼接方式在高频场景下的性能差异:

  1. + 操作符(语法糖,编译期转为 StringBuilder.append
  2. StringBuilder.append
  3. StringBuffer.append

🧰 项目创建与配置(Maven)

1. 创建基础工程

1
2
3
4
5
mvn archetype:generate \
  -DgroupId=com.xinchentechnote \
  -DartifactId=string-jmh \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false

2. 配置 pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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>

📄 测试代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 自动化测试配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)

方法吞吐量
StringBuilder0.478 ops/us
StringBuffer0.448 ops/us
+ 操作符🚫 0.199 ops/us

⏱ 平均耗时测试(AverageTime)

单位:每次操作的平均耗时(us/op)

方法平均耗时
StringBuilder2.021 us/op
StringBuffer2.237 us/op
+ 操作符🚫 5.244 us/op

🔍 原理解析

  • StringBuilder:非线程安全但性能最好,推荐在循环中使用。
  • StringBuffer:线程安全但性能略低。
  • + 操作符:虽然直观,但在循环中极其低效,会频繁创建临时对象,带来 GC 压力。

✅ 使用建议

拼接方式优点缺点推荐场景
StringBuilder性能最佳非线程安全单线程高频拼接
StringBuffer线程安全性能略差多线程拼接场景
+ 操作符简洁直观慢且 GC 压力大低频简易拼接

🏁 总结

本次 JMH 实测验证了开发经验中的最佳实践:在高频场景中,推荐使用 StringBuilder 进行字符串拼接。


💡 JMH 小知识:Java 微基准测试利器

JMH (Java Microbenchmark Harness) 是由 Oracle 和 OpenJDK 团队专为 Java 编写的微基准测试框架,用于衡量 Java 方法在纳秒到微秒级别的性能表现。JMH 特别适用于需要精细分析方法调用开销、编译优化、副作用等对性能影响的场景。

核心术语与注解解释:

注解或参数含义
@Benchmark标记要被测试的方法。每次运行都调用它并收集性能数据。
@BenchmarkMode设置基准测试的模式(如吞吐量、平均时间等)。可选值包括:- Throughput: 单位时间内操作次数- AverageTime: 每个操作平均耗时- SampleTime, SingleShotTime, AllModes
@OutputTimeUnit设置输出结果的时间单位,如 TimeUnit.MILLISECONDSMICROSECONDS
@State用于管理基准方法所需的状态变量作用域。常用 Scope.Thread 表示每线程独立状态。
@Fork设置执行几轮 JVM 启动来规避 JVM 热身阶段的影响,通常设置为 1~3。
@Warmup预热次数与每次持续时间(JIT 编译优化发生在此阶段)。避免冷启动影响基准数据。
@Measurement真正采集性能数据的次数和持续时间。建议至少 5 次 × 1s+

推荐配置参数说明:

1
2
3
4
5
6
@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);

确保了测得结果更真实、更接近应用实际表现。


希望本文能为你在日常开发与性能优化中提供量化参考! 作者:xinchen 首次发布日期:2025-05-25 版权所有 © 2025 作者信息:af83f787e8911dea9b3bf677746ebac9

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计