"abc") 編譯期進入 **類文件常量池** → 類加載時解析進 **運行時常量池** (Method Area/Metaspace) → **引用指向堆中 String 對象** (JDK 7+) 或常量池對象 (JDK 6-)。value 數組指向 **同一個字符數組** (JDK 7+ 去重) 或複製數組 (JDK 6-)。String.intern() 效率大幅提升。
JDK 6 (PermGen) JDK 7+ (Metaspace + Heap)
+------------------+ +------------------+
| PermGen | | Metaspace | ← 類元數據、運行時常量池 (符號引用)
| - 類元數據 | | - 類元數據 |
| - 運行時常量池 | 符號引用 解析 | - 運行時常量池 | 符號引用 → 指向堆對象
| - 字符串常量池 | ──────────► | (無字符串對象) |
+------------------+ +------------------+
| Heap | ← String 對象、char[]/byte[] 數組
| - String 對象 |
| - 字符數組 |
+------------------+
public class StringMemoryDemo {
public static void main(String[] args) {
// 1. 字面量:常量池唯一
String s1 = "Hello";
String s2 = "Hello";
System.out.println("s1 == s2: " + (s1 == s2)); // true (同一常量池引用)
// 2. new String:堆中新對象
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println("s3 == s4: " + (s3 == s4)); // false (不同堆對象)
System.out.println("s1 == s3: " + (s1 == s3)); // false (常量池 vs 堆)
// 3. intern() 註冊常量池
String s5 = s3.intern();
System.out.println("s1 == s5: " + (s1 == s5)); // true (s5 指向常量池對象)
System.out.println("s3 == s5: " + (s3 == s5)); // false
// 4. 拼接:編譯期常量折疊 vs 運行期 StringBuilder
String s6 = "Hel" + "lo"; // 編譯期常量折疊 → "Hello" (常量池)
String s7 = "Hel";
String s8 = s7 + "lo"; // 運行期 StringBuilder → new String (堆)
System.out.println("s1 == s6: " + (s1 == s6)); // true
System.out.println("s1 == s8: " + (s1 == s8)); // false
System.out.println("s8.intern() == s1: " + (s8.intern() == s1)); // true
// 5. 字節碼層面 ldc 指令
// s1/s2/s6: ldc #常量池索引 → 直接獲取常量池引用
// s3/s4/s8: new String + dup + ldc + invokespecial → 創建新對象
}
}
// JDK 8: char[] value (UTF-16, 2字節/字符)
// JDK 9+: byte[] value + byte coder (LATIN1=0/UTF16=1)
// 優勢:純 ASCII 字符串內存減半,GC 壓力降低
public final class String implements Serializable, Comparable, CharSequence {
@Stable private final byte[] value;
private final byte coder; // 0=LATIN1, 1=UTF16
private int hash; // 默認 0,首次計算後緩存
// 關鍵:共享字符數組 (JDK 7+ substring 不再共享,避免內存洩漏)
// JDK 6 substring 共享 value + offset + count → 長字符串 substring 導致原數組無法 GC
// JDK 7+ substring 新建字符數組複製 → 安全但微小開銷
}
| 參數 | 作用 | 建議場景 |
|---|---|---|
-XX:StringTableSize=1000003 | 設置字符串常量池哈希桶數 (質數),默認 60013 | 大量 intern() 調用、常量池碰撞嚴重時增大 (如 100萬+) |
-XX:+UseStringDeduplication | 啟用 G1 字符串去重 (JDK 8u20+) | 堆中大量重複字符串 (如日志、JSON 字段值),G1 GC 時自動合併相同 value 數組 |
-XX:+OptimizeStringConcat | 優化字符串拼接 (JDK 8 默認開啟) | 通常無需調整,JDK 9+ 已移除 (內建優化) |
-XX:-CompactStrings | 禁用緊湊字符串 (JDK 9+ 默認開啟) | 極少數 JNI/本地庫兼容性問題時禁用 |
-Xmx -Xms | 堆大小,間接影響常量池 (現已在堆中) | 預留足夠堆給字符串對象 |
+ 拼接:用 StringBuilder (非線程安全、高性能) 或 StringJoiner (Java 8+)。intern():如解析固定枚舉值、緩存 Key、數據庫枚舉列。注意:intern() 有全局鎖開銷,高併發慎用或分段鎖。null,幫助 GC。substring 持有長字符串引用 (JDK 6);JDK 7+ 已修復但會複製數組,超大字符串頻繁 substring 仍有開銷。byte[] 或 CharSequence:避免不必要的 String 創建 (如 Netty ByteBuf → String 解碼)。
// Netty/高性能網絡框架常用:堆外內存存儲字符串字節,零拷貝發送
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
directBuffer.put("Hello".getBytes(StandardCharsets.UTF_8));
directBuffer.flip();
// 發送時零拷貝,GC 不掃描堆外內存,需手動管理或 Cleaner
不屬於 Java 堆/常量池,受 -XX:MaxDirectMemorySize 限制。
Java 字符串內存模型經歷了 **PermGen 常量池 → Heap 常量池 (JDK 7) → Compact Strings (JDK 9)** 三次重大演進。核心掌握:**字面量進常量池、new 進堆、intern 註冊常量池、編譯期常量折疊、運行期 StringBuilder、Compact Strings 節省內存、StringDeduplication 去重**。調優抓手:StringTableSize、UseStringDeduplication、StringBuilder 替代 +、大對象及時釋放。理解本質,方能寫出高性能字符串處理代碼。