Skip to content

性能微优化-String

About 1192 wordsAbout 4 min

性能新书

2026-08-19

String

JDK9以前,对于使用“+”拼接字符串,会在编译代码时候转化成使用StringBuilder或者StringBuffer(更早版本),JDK9以后,则转化成调用StringConcatFactory类,由此类决定如何高效的实现字符串拼接而不在依赖JKD8之前通过编译转化来实现字符串拼接

拼接是String性能从JDK5到JDK8,到现在的JDK21以来一直性能优化地方。总结如下

JDK版本主要改动性能改善
JDK4之前提供StringBuffer用于拼接字符串,“+” 将编译阶段翻译成调用StringBuffer的append方法
JDK5提供StringBuilder用于拼接字符串,“+” 将编译阶段翻译成调用StringBuffer的append方法类似StringBuffer,但取消了同步锁功能
JDK9StringBuilder内部使用byte[] 维护字符串内容;"+"可选择翻译成使用StringConcatFactory来实现字符串拼接String是系统中使用频率最高的的一个类之一,String内部有一个char[]维护了字符串的内容。String最常用的操作是拼接,比如拼接多个字符串和变量成为一个较长字符串,输出到日志系统。byte[]数组减少内存占用,StringConcatFactory.makeConcatWithConstants在运行时刻再决定使用哪种字符串拼接优化方式
JDK23&JDK24优化StringConcatFactory当拼接的字符串参数过多时候,退化到直接使用StringBuilder

上面的表格在通过代码和字节码说明之前,需要先记住一点,通常字符串拼接,使用“+”即可,编译器在编译阶段会做优化。当然,如果是在循环体内这种需要拼接的字符串个数未知,建议显示的使用StringBuilder。

在JDK9之前,StringBuilder的用于拼接字符串,主要有2个性能问题

  • 未能估算容量,随着多次调用append方法,出现char[] 扩容消耗
  • 拼接完成通常会调用toString(),返回拼接后的字符串。String类的内部实现时候,会复制一份char[]

以如下代码为例子

int i=0;
String str = "abc"+i;

使用JDK8,编译成类似如下代码

int i=0;
StringBuilder temp = new StringBuilder();
temp.append("abc").append(i);
String str = temp.toString();

可以看到,要拼接的对象多,也就会多次调用append方法,导致StringBuilder的char[]或者byte[]多次扩容。另外,toString()内部实现,需要完整复制一份char[] 或者 byte[]

@Override
public String toString() {
    // 此构造函数内部会复制一份this.byte[]数组以避免修改StringBuilder内容,造成性能影响
    return new String(this);
}

JDK9后,按照JEP280的建议,调整了“+”,如果是已知拼接的对象个数,可以使用StringConcatFactory代替,它组要改善如下

  • StringConcatFactory内部允许直接使用byte[]数组构造返回的字符串,在使用StringBuilder时候,则需要重新复制一份byte[]
  • StringConcatFactory 支持精确计算的byte[] 容量,减少byte[] 扩容消耗
  • 如何拼接字符串通过makeConcatWithConstants方法的参数recipe参数来说明,recipe是一个字符串,包含了如如何获取拼接参数的说明,比如对于上例子,recipe是abc\u0001,其中abc表示字符串"abc",'\u001'表示从栈上获取一个参数,即参数i.

需要注意的是,如果手写StringBuilder代替"+"字符拼接 ,性能也可能略微不太一样,比如,对于如下同样使用StringBuilder拼接字符串

@Benchmark
public String concatByOptimizeBuilder(){
    //同 #concat(),这种字节码最少
    String c = new StringBuilder().append("sql:").append(a).append(b).toString();
    return c;
}



@Benchmark
public String concatByBuilder(){
    //字节码较多,性能慢
    StringBuilder sb = new StringBuilder();
    sb.append("sql:");
    sb.append(a);
    sb.append(b);
    return sb.toString();
}

测试结果如下,方法concatByOptimizeBuilder显示吞吐量比concatByBuilder 高很多

Benchmark                              Mode  Cnt      Score       Error   Units
StringConcat.concatByBuilder          thrpt    5  19916.860 ± 11182.162  ops/ms
StringConcat.concatByOptimizeBuilder  thrpt    5  57591.041 ±   347.212  ops/ms

如果分析其字节码,就会发现concatByBuilder生成的字节码,会多出一个入栈指令ALOAD 1,如下

// concatByOptimizeBuilder 
LDC "sql:"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;


//concatByBuilder
LDC "sql:"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
POP
ALOAD 1 //多出此加载指令,即sb对象

之所以concatByOptimizeBuilder指令没有ALOAD 1 ,这是因为StringBuilder::append方法会返回自身(已经在栈里了),无需再入栈。而concatByBuilder被Javac翻译成调用append方法之前,需要把变量sb入栈。这样的好处是即减少了一个指令,也可能因为CPU级别的缓存而提高性能。CPU缓存将在6.15说明

因此,如果在已知拼接个数的情况下,最好使用"+"来拼接,然后交给Java平台来优化其性能,目前版本如果需要性能优化,可以使用concatByOptimizeBuilder演示的方法拼接字符串获得更好的性能

知行合一