本文通過 javap -verbose 反編譯字節碼,結合多個案例,深入剖析字符串常量池(String Constant Pool)、CONSTANT_String_info、CONSTANT_Utf8_info 結構,以及 intern() 的運行時行為。
CONSTANT_String_info {
u1 tag = 8; // 標識字符串常量
u2 string_index; // 指向 CONSTANT_Utf8_info 的索引
}
CONSTANT_Utf8_info {
u1 tag = 1; // 標識 UTF-8 編碼字符串
u2 length; // 字節數組長度
u1 bytes[length]; // UTF-8 編碼後的字節數組
}
public class StringDemo {
private String s1 = "字符串S1";
}
javap -verbose 顯示:
#15 = Utf8 "字符串S1"(CONSTANT_Utf8_info)#2 = String #15(CONSTANT_String_info,string_index=#15)結論:字面量直接存入類常量池,引用指向 Utf8 結構。
private String s1 = "MyStr1"; private String s2 = "MyStr1";
常量池中僅有一個 Utf8 "MyStr1" 和一個 String #index,s1 和 s2 在編譯期指向同一常量池條目,運行時 s1 == s2 為 true。
private String s1 = new String("MyStr1");
字節碼顯示:
Utf8 "MyStr1" 和 String #index。java/lang/String."":(Ljava/lang/String;)V ,在堆上創建新對象。結果:s1 == "MyStr1" 為 false(堆對象 vs 常量池引用)。
public class StringDemo20 {
private String s1 = "MyStr1";
private String s2 = "MyStr1";
private String s3 = "My" + "Str1"; // 編譯期常量折疊
private String s01 = "My";
private String s02 = "Str1";
private String s4 = s01 + s02; // 運行期拼接,new StringBuilder().append().toString()
private String s5 = s4.intern(); // 手動入池
private static String str1 = "MyStr1";
private static String str2 = "MyStr1";
private static String str3 = "My" + "Str1";
private static String str01 = "My";
private static String str02 = "Str1";
private static String str4 = str01 + str02;
private static String str5 = str4.intern();
private static final String fstr1 = "MyStr1";
private static final String fstr2 = "MyStr1";
private static final String fstr3 = "My" + "Str1";
private static final String fstr01 = "My";
private static final String fstr02 = "Str1";
private static final String fstr4 = fstr01 + fstr02;
private static final String fstr5 = fstr4.intern();
class InnerDemo { /* 類似字段 */ }
public static void main(String[] args) {
StringDemo20 d = new StringDemo20();
// 大量 == 對比輸出...
}
}
| 表達式 | 結果 | 原因 |
|---|---|---|
| s1 == s2 | true | 同一字面量,編譯期指向同一常量池條目 |
| s1 == s3 | true | "My"+"Str1" 編譯期常量折疊為 "MyStr1" |
| s1 == s4 | false | s4 運行期 new StringBuilder 拼接,堆對象 |
| s1 == s5 | true | s4.intern() 將運行期字符串放入運行時常量池,返回池中引用 |
| static 字段同理 | true/false | 規則同實例字段,靜態字段在類加載時初始化 |
| final 字段 | true | 編譯期常量值傳播,等同字面量 |
| final 引用變量拼接 | false | 非常量變量拼接,運行期執行 |
intern() 將字符串複製到 PermGen,易 OOM。intern() 在池中記錄引用,不再複製字符數組,安全高效。intern() 可將運行期動態生成的字符串「入池」,使後續相同內容字符串共享引用。intern() 節省內存。intern()(有全局鎖開銷),考慮使用 ConcurrentHashMap 自建池。-XX:StringTableSize(默認 60013)調大以減少哈希衝突。理解常量池結構與 intern() 語義,是診斷字符串內存問題、優化大規模文本處理的關鍵。