首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

深入理解 Java 字串在記憶體中的儲存位置與機制

     編輯:Java 相關知識

前言

String 是 Java 中使用頻率最高的類別之一,但許多開發者對其記憶體儲存機制存在誤解。本文將從 JVM 記憶體模型、字串常量池演進、新舊建立方式差異等維度,系統性闡述 Java 字串在記憶體中的儲存位置與行為。

JVM 記憶體區域速覽

在討論 String 之前,先建立 JVM 記憶體的基本認知(以 HotSpot JVM 為例):

區域主要內容執行緒共享性生命週期
程式計數器當前執行指令位址私有執行緒存續
虛擬機棧區域變數表、運算棧、方法返回值私有執行緒存續
本地方法棧Native 方法執行狀態私有執行緒存續
堆物件實例、陣列共享GC 管理
方法區類別資訊、常量、靜態變數、JIT 編譯代碼共享JVM 存續
關鍵點:JDK 7 之前,字串常量池位於方法區(永久代);JDK 7 起移至堆中;JDK 8 移除永久代,改用元空間實作方法區。

字串常量池演進史

JDK 6 及之前:永久代

  • 常量池位於方法區(永久代)
  • 大小固定,難以調整,易導致 OutOfMemoryError: PermGen space
  • String.intern() 將字串放入永久代

JDK 7:移至堆中

  • Oracle 官方說明:interned strings are no longer allocated in the permanent generation
  • 常量池移至 Java 堆(新生代/老年代)
  • 受堆大小參數(-Xmx/-Xms)控制,彈性較大
  • 大量使用 intern() 的應用需調整堆大小

JDK 8+:元空間

  • 移除永久代,類別中繼資料存入元空間(Native Memory)
  • 字串常量池仍在堆中
  • 元空間大小預設無上限,受系統記憶體限制

字串建立方式與記憶體行為

方式一:字面值(雙引號)

String s1 = "hello";
String s2 = "hello";

執行流程:

  1. 編譯期:常量 "hello" 存入 .class 常量池
  2. 類載入時:解析常量池,在運行時常量池建立 String 物件
  3. 執行時:檢查常量池是否已存在 "hello"
  4. 存在 → 直接返回引用;不存在 → 建立新物件並存入

結果:s1 == s2 // true,指向同一個堆/常量池物件

方式二:new String()

String s3 = new String("hello");
String s4 = new String("hello");

執行流程:

  1. 類載入時:常量池已有 "hello"
  2. 執行 new String("hello"):在堆中建立新的 String 物件,內容複製自常量池
  3. 每次 new 都會建立獨立物件

結果:s3 == s4 // false,指向不同堆物件;但 s3.equals(s4) // true

方式三:intern() 手動入池

String s5 = new String("hello").intern();
String s6 = "hello";

行為:

  • 若常量池已有 "hello" → 返回常量池引用
  • 若無 → 在常量池建立並返回引用(JDK 7+ 在堆中建立)
  • 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()常量池是(手動入池)與常量池同引用

效能與最佳實踐建議

  1. 優先使用字面值:避免不必要的物件建立,利用常量池複用
  2. 拼接字串用 StringBuilder:迴圈中拼接避免大量中間物件
  3. 謹慎使用 intern():大量入池會增加常量池壓力,影響 GC;除非確定重複率高且生命週期長
  4. 比較內容用 equals():== 比較引用,equals() 比較內容
  5. 避免 new String(String):new String("abc") 多建立一個無意義物件

常見面試題解析

Q: String s = new String("abc") 建立了幾個物件?
A: 1 或 2 個。若常量池無 "abc" → 2 個(常量池 1 + 堆 1);若常量池已有 "abc" → 1 個(僅堆)。
Q: 為什麼 JDK 7 將常量池移到堆?
A: 永久代大小固定難調整,易 OOM;移至堆可受 -Xmx 控制,配合 GC 更靈活。
Q: String 是不可變的,有什麼好處?
A: 1) 線程安全 2) 常量池複用 3) HashCode 快取 4) 安全性(參數傳遞不被篡改) 5) 類載入器安全
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved