「常量池」是 Java 面試高頻考點,亦是理解 JVM 內存模型、字符串行為、類加載機制的核心鑰匙。本文將從理論分層、編譯期 vs 運行期、內存區域演進 (JDK 7/8+)、實戰驗證、OOM 再現 五個維度,徹底講透這個「經久不衰」的話題。
| 名稱 | 階段 | 存儲內容 | 位置 | 關鍵特點 |
|---|---|---|---|---|
| Class 文件常量池 (Constant Pool Table) | 編譯期 | 字面量、符號引用 (類/接口/字段/方法名、描述符) | .class 文件中 | 每個類一個,編譯器生成,javap -v 可查看 |
| 運行時常量池 (Runtime Constant Pool) | 類加載後 | Class 常量池內容 + 動態生成的常量 (如 String.intern()) | 方法區 (JDK 7: Heap / JDK 8+: Metaspace) | 每個類一個,具備動態性,String.intern() 可手動擴充 |
| 字符串常量池 (String Pool / String Table) | 運行期 | 字符串對象引用 (指向堆中實際字符串對象) | 堆 (JDK 7 起從永久代移至堆) | 全局單例表,String.intern() 核心操作對象,GC 可回收 |
核心關係:Class 常量池 (靜態) → 類加載 → 運行時常量池 (動態) → 解析符號引用 → 字符串常量池/堆對象。字符串常量池僅存引用,實際字符串對象在堆中。
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo"; // 編譯期常量折疊 → "Hello"
String s4 = "Hel" + new String("lo"); // 含 new,運行期拼接
String s5 = new String("Hello"); // 兩個對象:堆對象 + 常量池 "Hello"
String s6 = s5.intern(); // 手動驻留,返回池中引用
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8; // 變量拼接,運行期 StringBuilder
| 表達式 | 結果 | 原因 |
|---|---|---|
| s1 == s2 | true | 同一字面量,編譯期入池,引用同一對象 |
| s1 == s3 | true | 常量折疊優化,編譯期直接變為 "Hello" |
| s1 == s4 | false | 含 new String,運行期 StringBuilder 拼接,新對象在堆 |
| s1 == s9 | false | 變量拼接,運行期新建對象 |
| s4 == s5 | false | 兩個獨立 new 操作,堆中不同對象 |
| s1 == s6 | true | intern() 返回池中已有 "Hello" 的引用 |
StringBuilder.append() → toString() 新建堆對象,不自動入池。| 版本 | 運行時常量池位置 | 字符串常量池位置 | 關鍵變化 |
|---|---|---|---|
| JDK 6 | 永久代 | 永久代 | 受 -XX:MaxPermSize 限制,易 OOM |
| JDK 7 | 堆 | 堆 | 字符串池移出永久代,受堆 -Xmx 限制,GC 可回收 |
| JDK 8+ | 元空間 | 堆 | 類元數據移至元空間 (Native Memory),受 -XX:MaxMetaspaceSize 限制;字符串池仍在堆 |
// JDK 7: -XX:MaxPermSize=10M
// JDK 8: -XX:MaxMetaspaceSize=10M
List list = new ArrayList<>();
int i = 0;
while (true) {
// 手動驅動驻留,持續擴充運行時常量池/字符串池
list.add(String.valueOf(i++).intern());
}
// JDK 7 拋出: java.lang.OutOfMemoryError: PermGen space
// JDK 8 拋出: java.lang.OutOfMemoryError: Metaspace
// 證明:運行時常量池 (含類元數據) 在 JDK 7 在永久代,JDK 8 在元空間
// 字符串池在堆,受 -Xmx 控制,上述代碼不會直接導致堆 OOM (除非 list 撐爆堆)
包裝類緩存範圍 (自動裝箱時復用):
Integer / Short / Byte / Long:-128 ~ 127 (可通過 -XX:AutoBoxCacheMax 調整 Integer 上限)。Character:0 ~ 127 (ASCII 範圍)。Boolean:TRUE / FALSE 兩個單例。Float / Double:無緩存,每次裝箱新建對象。Integer a = 100, b = 100; // true (緩存範圍內)
Integer c = 200, d = 200; // false (超出緩存,新建對象)
Integer e = 200; e == new Integer(200); // false (new 強制新建)
StringBuilder (單線程) / StringBuffer (多線程),避免產生大量臨時對象佔用堆/觸發 GC。intern() 會擴充字符串池 (堆),增加 GC 掃描開銷,甚至導致 OOM。僅在極高重複率、生命週期長的業務 Key (如股票代碼、枚舉值) 場景考慮。static final String CONST = "VALUE"; 編譯期常量折疊,調用類直接內聯常量值,無運行期解析開銷。intern()。Metaspace 使用率,防止類加載洩漏 (動態代碼生成、熱部署、Groovy/Kotlin 腳本) 導致 Metaspace OOM。
[Java 源碼]
│
▼ 編譯 (javac)
[.class 文件] ──→ Class 常量池 (字面量、符號引用)
│
▼ 類加載 (ClassLoader)
[方法區 / 元空間] ──→ 運行時常量池 (動態、可擴充)
│
├─→ 符號引用解析 ──→ [堆] 字符串對象
│ ▲
│ │ 引用
└─→ String.intern() ───────┘
[堆] 字符串常量池 (String Table - Hash表)
徹底理解三層常量池分工、編譯期優化邏輯、JDK 7/8 內存區域重構、intern() 動態驻留機制,才能在面試中游刃有餘,在生產排查 OOM、字符串重複創建性能問題時精準定位根因。常量池不再是「背誦題」,而是 JVM 內存模型的核心骨架。