String 是 Java 中使用頻率最高的類別之一,但許多開發者對其記憶體儲存機制存在誤解。本文將從 JVM 記憶體模型、字串常量池演進、新舊建立方式差異等維度,系統性闡述 Java 字串在記憶體中的儲存位置與行為。
在討論 String 之前,先建立 JVM 記憶體的基本認知(以 HotSpot JVM 為例):
| 區域 | 主要內容 | 執行緒共享性 | 生命週期 |
|---|---|---|---|
| 程式計數器 | 當前執行指令位址 | 私有 | 執行緒存續 |
| 虛擬機棧 | 區域變數表、運算棧、方法返回值 | 私有 | 執行緒存續 |
| 本地方法棧 | Native 方法執行狀態 | 私有 | 執行緒存續 |
| 堆 | 物件實例、陣列 | 共享 | GC 管理 |
| 方法區 | 類別資訊、常量、靜態變數、JIT 編譯代碼 | 共享 | JVM 存續 |
關鍵點:JDK 7 之前,字串常量池位於方法區(永久代);JDK 7 起移至堆中;JDK 8 移除永久代,改用元空間實作方法區。
OutOfMemoryError: PermGen spaceString.intern() 將字串放入永久代interned strings are no longer allocated in the permanent generationintern() 的應用需調整堆大小String s1 = "hello";
String s2 = "hello";
執行流程:
.class 常量池結果:s1 == s2 // true,指向同一個堆/常量池物件
String s3 = new String("hello");
String s4 = new String("hello");
執行流程:
new String("hello"):在堆中建立新的 String 物件,內容複製自常量池new 都會建立獨立物件結果:s3 == s4 // false,指向不同堆物件;但 s3.equals(s4) // true
String s5 = new String("hello").intern();
String s6 = "hello";
行為:
s5 == s6 // true┌─────────────────────────────────────────────────────────────┐ │ JVM Heap (JDK 7+) │ │ ┌─────────────────────┐ ┌─────────────────────────────┐ │ │ │ String Constant │ │ Heap Objects │ │ │ │ Pool (Table) │ │ │ │ │ │ ┌───────────────┐ │ │ String@1000 "hello" ←─────┼─┤ │ │ │ "hello" ──────┼──┼────┤ String@2000 "hello" (new) │ │ │ │ │ "world" ──────┼──┼──┐ │ String@3000 "world" (new) │ │ │ │ └───────────────┘ │ │ │ │ │ │ └─────────────────────┘ │ │ │ │ │ ▲ │ │ │ │ │ │ intern() │ │ │ │ │ │ ▼ ▼ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Stack Frame (Local Variables) │ │ │ │ s1 ────────────────────────────────────────────┐ │ │ │ │ s2 ────────────────────────────────────────┐ │ │ │ │ │ s3 ──────────────────────────────────┐ │ │ │ │ │ s4 ────────────────────────────┐ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
public class StringMemoryDemo {
public static void main(String[] args) {
// 字面值
String a = "hello";
String b = "hello";
System.out.println("a == b: " + (a == b)); // true
System.out.println("a.equals(b): " + a.equals(b)); // true
// new 建立
String c = new String("hello");
String d = new String("hello");
System.out.println("c == d: " + (c == d)); // false
System.out.println("c.equals(d): " + c.equals(d)); // true
// 混合比較
System.out.println("a == c: " + (a == c)); // false
System.out.println("a.equals(c): " + a.equals(c)); // true
// intern()
String e = new String("hello").intern();
System.out.println("a == e: " + (a == e)); // true
System.out.println("c == e: " + (c == e)); // false
// 運算時期建立
String f = "hel" + "lo"; // 編譯期常量折疊 → "hello"
String g = "hel";
String h = g + "lo"; // 運算期 new StringBuilder → toString()
System.out.println("a == f: " + (a == f)); // true
System.out.println("a == h: " + (a == h)); // false
}
}
| 場景 | 物件建立位置 | 是否複用 | == 比較結果 |
|---|---|---|---|
| 字面值 "abc" | 常量池(堆中) | 是 | true |
| new String("abc") | 堆 | 否(每次新建) | false |
| 字面值 + 字面值 | 常量池(編譯期優化) | 是 | true |
| 變數 + 字面值/變數 | 堆(StringBuilder) | 否 | false |
| intern() | 常量池 | 是(手動入池) | 與常量池同引用 |
== 比較引用,equals() 比較內容new String("abc") 多建立一個無意義物件