參考:https://www.jianshu.com/p/160c9be0b132java
字符串變量(非final修飾)經過 "+" 進行拼接,在編譯過程當中會轉化爲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 的執行效果實際上是同樣的,不過在for循環中,千萬不要使用 "+" 進行字符串拼接。併發
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循環中使用 "+" 和StringBuilder進行1萬次字符串拼接,耗時狀況以下:
一、使用 "+" 拼接,平均耗時 250ms;
二、使用StringBuilder拼接,平均耗時 1ms;app
for循環中使用 "+" 拼接爲何這麼慢?下面是run1方法的字節碼指令ide
5 ~ 34 行對應for循環的代碼,能夠發現,每次循環都會從新初始化StringBuilder對象,致使性能問題的出現。性能
StringBuilder內部維護了一個char[]類型的value,用來保存經過append方法添加的內容,經過 new StringBuilder()
初始化時,char[]的默認長度爲16,若是append第17個字符,會發生什麼?測試
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的剩餘容量,沒法添加所有內容,則經過expandCapacity(int minimumCapacity)
方法對value進行擴容,其中minimumCapacity = 原value長度 + append添加的內容長度。
一、擴大容量爲原來的兩倍 + 2,爲何要 + 2,而不是恰好兩倍?
二、若是擴容以後,仍是沒法添加所有內容,則將 minimumCapacity 做爲最終的容量大小;
三、利用 System.arraycopy
方法對原value數據進行復制;ui
在使用StringBuilder時,若是給定一個合適的初始值,能夠避免因爲char[]數組屢次複製而致使的性能問題。spa
不一樣初始容量的性能測試:code
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; } }
執行一千萬次append操做,不一樣初始容量的耗時狀況以下:
一、容量爲默認16時,平均耗時110ms;
二、容量爲40000000時,不會發生複製操做,平均耗時85ms;
經過以上數據能夠發現,性能損耗不是很嚴重。
一、StringBuilder內部進行擴容時,會新建一個大小爲原來兩倍+2的char數組,並複製原char數組到新數組,致使內存的消耗,增長GC的壓力。
二、StringBuilder的toString方法,也會形成char數組的浪費。
public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
String的構造方法中,會新建一個大小相等的char數組,並使用 System.arraycopy()
複製StringBuilder中char數組的數據,這樣StringBuilder的char數組就白白浪費了。
重用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數組的內存區域設置爲0,這樣char數組重複使用,爲了不併發訪問,能夠在ThreadLocal中使用StringBuilderHolder,使用方式以下:
private static final ThreadLocal<StringBuilderHolder> stringBuilder= new ThreadLocal<StringBuilderHolder>() { @Override protected StringBuilderHolder initialValue() { return new StringBuilderHolder(256); } }; StringBuilder sb = stringBuilder.get().resetAndGet();
不過這種方式也存在一個問題,該StringBuilder實例的內存空間一直不會被GC回收,若是char數組在某次操做中被擴容到一個很大的值,可能以後很長一段時間都不會用到如此大的空間,就會形成內存的浪費。
雖然使用默認的StringBuilder進行字符串拼接操做,性能消耗不是很嚴重,但在高性能場景下,仍是推薦使用ThreadLocal下可重用的StringBuilder方案。