Java 開發者都知道「迴圈中用 StringBuffer 避免 String 串接效能問題」,但極少人實際看過編譯後的位元組碼差異。本文實戰示範如何使用 JDK 內建工具 javap 反組譯類別檔案,從 JVM 指令層面揭示效能差異的根本原因。
-c 反組譯方法體、-v 詳細資訊、-p 顯示私有成員測試類別 JavapTip 包含兩個靜態方法,功能相同:回傳 "012...n" 字串。
public class JavapTip {
// String 串接版本
private static String withStrings(int count) {
String s = "";
for (int i = 0; i < count; i++) {
s += i; // 編譯器語法糖
}
return s;
}
// StringBuffer 版本
private static String withStringBuffer(int count) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < count; i++) {
sb.append(i);
}
return sb.toString();
}
}
Method java.lang.String withStrings(int)
0: ldc #2 // String ""
2: astore_1
3: iconst_0
4: istore_2
5: goto 30
8: new #3 // class java/lang/StringBuilder
11: dup
12: invokespecial #4 // Method java/lang/StringBuilder."":()V
15: aload_1
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: iload_2
20: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
23: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: astore_1
27: iinc 2, 1
30: iload_2
31: iload_0
32: if_icmplt 8
35: aload_1
36: areturn
Method java.lang.String withStringBuffer(int)
0: new #3 // class java/lang/StringBuffer
3: dup
4: invokespecial #4 // Method java/lang/StringBuffer."":()V
7: astore_1
8: iconst_0
9: istore_2
10: goto 22
13: aload_1
14: iload_2
15: invokevirtual #6 // Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
18: pop
19: iinc 2, 1
22: iload_2
23: iload_0
24: if_icmplt 13
27: aload_1
28: invokevirtual #7 // Method java/lang/StringBuffer.toString:()Ljava/lang/String;
31: areturn
| 指標 | withStrings (String +=) | withStringBuffer (sb.append) |
|---|---|---|
| 迴圈內新建物件 | 每次迴圈 new StringBuilder (位元組碼 8-12) | 零新建,複用同一實例 |
| 字串複製次數 | 每次迴圈複製現有字串內容到新 Builder | 無複製,直接追加 |
| 方法呼叫次數 | 每次迴圈 3 次虛擬呼叫 (append×2 + toString) | 每次迴圈 1 次虛擬呼叫 (append) |
| 記憶體分配壓力 | O(n²) 字元陣列分配與複製 | O(n) 擴容 (指數增長策略) |
| GC 壓力 | 大量短命物件觸發 Minor GC | 極低 |
s += i 編譯器自動轉換為:
new StringBuilder()
.append(s)
.append(i)
.toString()
關鍵洞見:每次迴圈都會 new StringBuilder,這是效能殺手的根本原因。JDK 1.5+ 使用 StringBuilder (非同步),JDK 1.4 使用 StringBuffer (同步)。
private static String withStringBuilder(int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(i);
}
return sb.toString();
}
反組譯結果與 StringBuffer 幾乎相同,差異僅在 invokespecial 目標類別。單執行緒環境 StringBuilder 效能略優 (無同步開銷)。
# 顯示所有資訊 (常數池、行號表、區域變數表、簽名屬性...)
javap -v JavapTip
# 只顯示公有/受保護成員 (預設)
javap JavapTip
# 顯示私有成員
javap -p JavapTip
# 反組譯並顯示行號對應
javap -c -l JavapTip
# 輸出到檔案
javap -c JavapTip > JavapTip.asm
javap 是 JDK 內建的「X 光機」,讓我們穿透語法糖直視 JVM 執行真相。本文實驗證明:String 串接每次迴圈隱含 new StringBuilder + 複製舊內容 + toString,而 StringBuffer/StringBuilder 單一實例複用。雖然現代 JIT 編譯器 (Escape Analysis、String Concatenation Optimization) 能部分優化,但顯式使用 StringBuilder 仍是高效能 Java 程式碼的鐵律。養成定期用 javap 驗證關鍵路徑代碼的習慣,將讓你從「知道最佳實踐」進化為「理解最佳實踐的底層邏輯」。