字串常量池演進
| 版本 | 常量池位置 | intern() 行為 |
| JDK 6 | 永久代 | 不存在則複製字串到常量池 |
| JDK 7+ | 堆內存 | 不存在則存入引用指向堆中字串 |
核心差異示意
String s = new String("abc"); // 堆中建立 "abc" 物件
s.intern();
// JDK 6: 永久代常量池新建 "abc" 副本,返回副本引用
// JDK 7+: 堆常量池存入 s 的引用,返回 s 引用
經典驗證案例
public class InternTest {
public static void main(String[] args) {
// 案例 1:運算式產生的字串
String str = "str" + new String("01"); // 堆中新物件 "str01"
str.intern(); // 註冊到常量池
String str1 = "str01"; // 常量池直接取得
System.out.println(str == str1); // JDK6: false | JDK7+: true
// 案例 2:new String() 後再 intern
String str2 = new String("str01"); // 堆中新物件
str2.intern(); // 常量池已有 "str01"(來自 str1)
String str3 = "str01";
System.out.println(str2 == str3); // false(str2 指向堆舊物件)
// 案例 3:純字面量組合
String str4 = "str01";
String str5 = new String("str") + new String("01"); // 堆新物件
str5.intern();
System.out.println(str4 == str5); // false
}
}
執行結果對照
| 案例 | JDK 6 | JDK 7/8/11/17+ | 關鍵原因 |
| 案例 1 | false | true | JDK7+ intern 存引用,str 和 str1 指向同一堆物件 |
| 案例 2 | false | false | str2 建立時常量池已有引用,intern 不改變 str2 指向 |
| 案例 3 | false | false | str5 指向堆新物件,intern 只登記引用不改變 str5 指向 |
記憶體模型圖解
JDK 6
堆內存: [String "str01"@0x100] ← str
永久代常量池: ["str01"@0x200] ← str1, intern() 複製體
JDK 7+
堆內存: [String "str01"@0x100] ← str, str1, intern() 登記引用
堆常量池: [ref@0x100] ← 指向同一物件
實務指導原則
- 不要依賴
intern() 進行字串比較:永遠用 equals()
- 大量重複字串可考慮 intern:如解析 XML/JSON 產生大量相同標籤名,可減少記憶體佔用
- JDK 7+ 常量池在堆中:受
-Xmx 限制,大量 intern 可能導致 OOM,需監控
- 字面量自動入池:
"abc" 編譯期進入常量池,new String("abc") 執行期在堆建立
- 編譯期優化:
"a" + "b" → "ab"(常量折疊),"a" + new String("b") → 堆中新物件
常見誤區
| 誤區 | 真相 |
| intern() 會修改原字串引用 | String 不可變,intern() 只返回常量池引用,不改變呼叫者引用 |
| 所有字串都在常量池 | 只有字面量和調用 intern() 的字串(引用)在常量池 |
| JDK 7+ intern() 會複製字串 | 只存引用,不複製字元陣列 |
總結
String.intern() 是手動將字串引用登記到全局常量池的機制。JDK 7 將常量池從永久代移至堆,並改為存儲引用而非複製,導致運算式產生的字串經 intern() 後可與字面量共享同一物件。理解這一演進,有助於排查字串比較異常、優化大量重複字串記憶體佔用,但日常開發堅持使用 equals() 比較內容。