Java中String、StringBufferr和StringBuilde三种字符串操作的性能比较

tech2025-05-25  15

JMH简介 MH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。 JMH 比较典型的应用场景如下:

想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性对比接口不同实现在给定条件下的吞吐量查看多少百分比的请求在多长时间内完成

加入依赖

<dependencies> <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core --> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>1.19</version> </dependency> <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess --> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.19</version> </dependency> </dependencies>

编写基准测试

@BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 3, time = 1) @Measurement(iterations = 3, time = 5) @Threads(4) @Fork(1) @State(value = Scope.Benchmark) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class StringConnectTest { @Param(value = {"10", "50", "100"}) private int length; @Benchmark public void testStringAdd(Blackhole blackhole) { String a = ""; for (int i = 0; i < length; i++) { a += i; } blackhole.consume(a); } @Benchmark public void testStringBuffer(Blackhole blackhole) { StringBuffer buffer=new StringBuffer(); for (int i = 0; i < length; i++) { buffer.append(i); } blackhole.consume(buffer.toString()); } @Benchmark public void testStringBuilderAdd(Blackhole blackhole) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { sb.append(i); } blackhole.consume(sb.toString()); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(StringConnectTest.class.getSimpleName()) .result("result.json") .resultFormat(ResultFormatType.JSON).build(); new Runner(opt).run(); } }

测试结果 该部分显示测量迭代的情况,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。在进行 5 次迭代后,进行统计,在本例中,length 为 100 的情况下 testStringBuilderAdd 方法的平均执行花费时间为 944.588ns,误差为 263.442 ns。 JMH可视化

JMH visual Chart JMH Visualizer

性能分析

String 的字符串连接操作为什么慢呢? 这是因为每一个字符串连接的操作(a += i),都需要创建一个新的 String 对象,然后再销毁,再创建。这种模式对CPU 和内存消耗都比较大。

StringBuilder 和 StringBuffer 为什么快呢?因为 StringBuilder 和 StringBuffer 的内部实现,预先分配了一定的内存。字符串操作时,只有预分配内存不足,才会扩展内存,这就大幅度减少了内存分配、拷贝和释放的频率。

StringBuilder 为什么比 StringBuffer 还要快呢?StringBuffer 的字符串操作是多线程安全的,而 StringBuilder 的操作就不是。

@Override public synchronized StringBuffer append(int i) { toStringCache = null; super.append(i); return this; } @Override public StringBuilder append(int i) { super.append(i); return this; }

即使在单线程条件下使用synchronized关键字也会有性能损害,所以线程同步,就是StringBuffer比StringBuilder慢的原因

结论 1. 频繁的对象创建、销毁,有损代码的效率; 2. 减少内存分配、拷贝、释放的频率,可以提高代码的效率; 3. 即使是单线程环境,使用线程同步依然有损代码的效率

从上面的结论中我们是不是只用Stringbuilder来进行字符操作?测试字符串的拼接案例

字符拼接基准测试

@Benchmark public void testStringAdd() { for (int i = 0; i < length; i++) { String a="hello"+"world"; } } @Benchmark public void testStringBuilderAdd() { for (int i = 0; i < length; i++) { StringBuilder builder=new StringBuilder(); builder.append("hello"); builder.append("world"); } }

变量对于字符拼接的基准测试

@Benchmark public void testVariableStringAdd() { for (int i = 0; i < length; i++) { String a="hello"+getVariable(); } } @Benchmark public void testVariableStringBuilderAdd() { for (int i = 0; i < length; i++) { StringBuilder builder=new StringBuilder(); builder.append("hello"); builder.append(getVariable()); } } private String getVariable(){ return "world"; }

性能结果 这个巨大的差异,主要来自于 Java 编译器和 JVM 对字符串处理的优化。" Hello, " + “world! " 这样的表达式,并没有真正执行字符串连接。编译器会把它处理成一个连接好的常量字串"Hello,world!”。这样,也就不存在反复的对象创建和销毁了,常量字符串的连接显示了超高的效率。

总结

1. Java 的编译器会优化常量字符串的连接,我们可以放心地把长的字符串换成多行; 2. 带有变量的字符串连接,StringBuilder 效率更高。如果效率敏感的代码,建议使用 StringBuilder。String 的连接操作可读性更高,效率不敏感的代码可以使用,比如异常 信息、调试日志、使用不频繁的代码; 3. 如果涉及大量的字符串操作,使用 StringBuilder 效率更高; 4. 除非有线程安全的需求,不推荐使用线程安全的 StringBuffer。

JMH工具的使用

最新回复(0)