簡書 占小狼
轉(zhuǎn)載請注明原創(chuàng)出處,謝謝炕檩!
連接符號 "+" 本質(zhì)
在 淺談Java String內(nèi)幕(1) 中斗蒋,字符串變量(非final修飾)通過 "+" 進(jìn)行拼接捌斧,在編譯過程中會轉(zhuǎn)化為StringBuilder對象的append操作,注意是編譯過程泉沾,而不是在JVM中捞蚂。
public class StringTest {
public static void main(String[] args) {
String str1 = "hello ";
String str2 = "java";
String str3 = str1 + str2 + "!";
String str4 = new StringBuilder().append(str1).append(str2).append("!").toString();
}
}
上述 str3 和 str4 的執(zhí)行效果其實(shí)是一樣的,不過在for循環(huán)中跷究,千萬不要使用 "+" 進(jìn)行字符串拼接姓迅。
public class test {
public static void main(String[] args) {
run1();
run2();
}
public static void run1() {
long start = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}
System.out.println(System.currentTimeMillis() - start);
}
public static void run2() {
long start = System.currentTimeMillis();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append(i);
}
System.out.println(System.currentTimeMillis() - start);
}
}
在for循環(huán)中使用 "+" 和StringBuilder進(jìn)行1萬次字符串拼接,耗時(shí)情況如下:
1俊马、使用 "+" 拼接丁存,平均耗時(shí) 250ms;
2柴我、使用StringBuilder拼接解寝,平均耗時(shí) 1ms;
for循環(huán)中使用 "+" 拼接為什么這么慢艘儒?下面是run1方法的字節(jié)碼指令
5 ~ 34 行對應(yīng)for循環(huán)的代碼聋伦,可以發(fā)現(xiàn),每次循環(huán)都會重新初始化StringBuilder對象界睁,導(dǎo)致性能問題的出現(xiàn)觉增。
性能問題
StringBuilder內(nèi)部維護(hù)了一個(gè)char[]類型的value,用來保存通過append方法添加的內(nèi)容晕窑,通過 new StringBuilder()
初始化時(shí)抑片,char[]的默認(rèn)長度為16,如果append第17個(gè)字符杨赤,會發(fā)生什么敞斋?
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
如果value的剩余容量,無法添加全部內(nèi)容疾牲,則通過expandCapacity(int minimumCapacity)
方法對value進(jìn)行擴(kuò)容植捎,其中minimumCapacity = 原value長度 + append添加的內(nèi)容長度。
1阳柔、擴(kuò)大容量為原來的兩倍 + 2焰枢,為什么要 + 2,而不是剛好兩倍舌剂?
2济锄、如果擴(kuò)容之后,還是無法添加全部內(nèi)容霍转,則將 minimumCapacity 作為最終的容量大屑鼍;
3避消、利用 System.arraycopy
方法對原value數(shù)據(jù)進(jìn)行復(fù)制低滩;
在使用StringBuilder時(shí)召夹,如果給定一個(gè)合適的初始值,可以避免由于char[]數(shù)組多次復(fù)制而導(dǎo)致的性能問題恕沫。
不同初始容量的性能測試:
public class StringBuilderTest {
public static void main(String[] args) {
int sum = 0;
final int capacity = 40000000;
for (int i = 0; i < 100; i++) {
sum += cost(capacity);
}
System.out.println(sum / 100);
}
public static long cost(int capacity) {
long start = System.currentTimeMillis();
StringBuilder builder = new StringBuilder(capacity);
for (int i = 0; i < 10000000; i++) {
builder.append("java");
}
return System.currentTimeMillis() - start;
}
}
執(zhí)行一千萬次append操作监憎,不同初始容量的耗時(shí)情況如下:
1、容量為默認(rèn)16時(shí)婶溯,平均耗時(shí)110ms鲸阔;
2、容量為40000000時(shí)爬虱,不會發(fā)生復(fù)制操作隶债,平均耗時(shí)85ms腾它;
通過以上數(shù)據(jù)可以發(fā)現(xiàn)跑筝,性能損耗不是很嚴(yán)重。
內(nèi)存問題
1瞒滴、StringBuilder內(nèi)部進(jìn)行擴(kuò)容時(shí)曲梗,會新建一個(gè)大小為原來兩倍+2的char數(shù)組,并復(fù)制原char數(shù)組到新數(shù)組妓忍,導(dǎo)致內(nèi)存的消耗虏两,增加GC的壓力。
2世剖、StringBuilder的toString方法定罢,也會造成char數(shù)組的浪費(fèi)。
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
String的構(gòu)造方法中旁瘫,會新建一個(gè)大小相等的char數(shù)組祖凫,并使用 System.arraycopy()
復(fù)制StringBuilder中char數(shù)組的數(shù)據(jù),這樣StringBuilder的char數(shù)組就白白浪費(fèi)了酬凳。
重用StringBuilder
public class StringBuilderHolder {
private final StringBuilder sb;
public StringBuilderHolder(int capacity) {
sb = new StringBuilder(capacity);
}
public StringBuilder resetAndGet() {
sb.setLength(0);
return sb;
}
}
通過 sb.setLength(0)
方法可以把char數(shù)組的內(nèi)存區(qū)域設(shè)置為0惠况,這樣char數(shù)組重復(fù)使用,為了避免并發(fā)訪問宁仔,可以在ThreadLocal中使用StringBuilderHolder稠屠,使用方式如下:
private static final ThreadLocal<StringBuilderHolder> stringBuilder= new ThreadLocal<StringBuilderHolder>() {
@Override
protected StringBuilderHolder initialValue() {
return new StringBuilderHolder(256);
}
};
StringBuilder sb = stringBuilder.get().resetAndGet();
不過這種方式也存在一個(gè)問題,該StringBuilder實(shí)例的內(nèi)存空間一直不會被GC回收翎苫,如果char數(shù)組在某次操作中被擴(kuò)容到一個(gè)很大的值权埠,可能之后很長一段時(shí)間都不會用到如此大的空間,就會造成內(nèi)存的浪費(fèi)煎谍。
總結(jié)
雖然使用默認(rèn)的StringBuilder進(jìn)行字符串拼接操作攘蔽,性能消耗不是很嚴(yán)重,但在高性能場景下粱快,還是推薦使用ThreadLocal下可重用的StringBuilder方案秩彤。
參考資料:
StringBuilder在高性能場景下的正確用法
END叔扼。
我是占小狼。
在魔都艱苦奮斗漫雷,白天是上班族瓜富,晚上是知識服務(wù)工作者。
如果讀完覺得有收獲的話降盹,記得關(guān)注和點(diǎn)贊哦与柑。
非要打賞的話,我也是不會拒絕的蓄坏。