性能微优化-String
String
JDK9以前,对于使用“+”拼接字符串,会在编译代码时候转化成使用StringBuilder或者StringBuffer(更早版本),JDK9以后,则转化成调用StringConcatFactory类,由此类决定如何高效的实现字符串拼接而不在依赖JKD8之前通过编译转化来实现字符串拼接
拼接是String性能从JDK5到JDK8,到现在的JDK21以来一直性能优化地方。总结如下
| JDK版本 | 主要改动 | 性能改善 |
|---|---|---|
| JDK4之前 | 提供StringBuffer用于拼接字符串,“+” 将编译阶段翻译成调用StringBuffer的append方法 | |
| JDK5 | 提供StringBuilder用于拼接字符串,“+” 将编译阶段翻译成调用StringBuffer的append方法 | 类似StringBuffer,但取消了同步锁功能 |
| JDK9 | StringBuilder内部使用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演示的方法拼接字符串获得更好的性能
