Java 記憶體模型(JMM)是理解物件生命週期、效能調優、避免 OOM 的基石。本文系統梳理堆、虛擬機棧、常量池、方法區、本地方法棧、程式計數器六大運行時資料區,並透過代碼實戰演示物件分配與引用關係。
一、JVM 運行時資料區總覽
| 區域 | 線程私有/共享 | 核心內容 | 異常 |
| 程式計數器 | 私有 | 當前執行位元碼指令地址 | 無 |
| 虛擬機棧 | 私有 | 棧幀(局部變數表、操作數棧、動態連結、返回地址) | StackOverflowError / OOM |
| 本地方法棧 | 私有 | Native 方法調用 | StackOverflowError / OOM |
| 堆 | 共享 | 幾乎所有物件實例、陣列 | OutOfMemoryError |
| 方法區 | 共享 | 類別元資料、常量、靜態變數、JIT 編譯碼 | OutOfMemoryError |
| 運行時常量池 | 共享(屬方法區) | 編譯期常量、符號引用、字串常量池 | OOM |
JDK 8 移除永久代,類別元資料移至元空間;字串常量池、靜態變數移至堆中。
二、核心區域深度解析
1. 虛擬機棧:方法執行的基本單位
- 每個方法調用對應一個棧幀入棧/出棧。
- 局部變數表:基本型別、物件引用、returnAddress。長度在編譯期確定。
- 操作數棧:位元碼指令操作數暫存區。
- 棧深度過大 →
StackOverflowError;無法申請記憶體 → OOM。
2. 堆:物件分配主戰場
- 新生代:Eden + 2 Survivor,Minor GC 高頻。
- 老年代:大物件、長期存活物件,Major GC/Full GC 低頻。
- TLAB(執行緒本地分配緩衝)優化多執行緒分配效能。
3. 常量池:編譯期與執行期的橋樑
- Class 常量池:.class 檔案中,存字面量、符號引用。
- 運行時常量池:類別載入後進入方法區,具備動態性(
String.intern() 可擴充)。
- 字串常量池:JDK 7+ 移至堆中,存放字串字面量及
intern() 字串。
三、代碼實戰:物件分配與引用關係圖解
案例代碼
class BirthDate {
private int day, month, year;
public BirthDate(int d, int m, int y) { day = d; month = m; year = y; }
}
public class Test {
public static void main(String[] args) {
int date = 9; // 局部基本型別 → 棧
Test test = new Test(); // test引用→棧,物件→堆
test.change(date); // 參數i→棧,基本型別傳值
BirthDate d1 = new BirthDate(7,7,1970); // d1引用→棧,物件→堆
}
public void change(int i) { i = 1234; } // i為局部變數,棧中,方法結束銷毀
}
記憶體狀態演進
int date = 9:棧 分配局部變數 date,值 9。
Test test = new Test():棧 分配引用 test;堆 分配 Test 物件。
test.change(date):棧 新建棧幀,參數 i 接收值 9(值傳遞)。修改 i 不影響 date。方法結束,棧幀彈出,i 消失。
BirthDate d1 = new BirthDate(7,7,1970):棧 分配引用 d1;堆 分配 BirthDate 物件(含欄位 day/month/year)。建構子參數 d,m,y 在棧中,建構子結束銷毀。
main 結束:棧 中 date、test、d1 引用銷毀;堆 中 Test、BirthDate 物件等待 GC 回收。
四、字串與基本型別常量的特殊規則
字串:字面量 vs new
String s1 = "china"; // 常量池
String s2 = "china"; // 常量池,同一引用
String s3 = "china"; // 常量池,同一引用
// s1 == s2 == s3 → true
String ss1 = new String("china"); // 堆新物件
String ss2 = new String("china"); // 堆另一物件
// ss1 == ss2 → false;ss1.equals(ss2) → true
基本型別包裝類別緩存(-128~127)
Integer a = 100, b = 100; // 緩存池,a == b → true
Integer c = 200, d = 200; // 新物件,c == d → false
// 統一用 equals() 比較值
五、成員變數 vs 局部變數記憶體差異
| 維度 | 成員變數(欄位) | 局部變數 |
| 位置 | 堆中物件內部 | 棧幀局部變數表 |
| 生命週期 | 隨物件創建/銷毀 | 隨方法進入/退出 |
| 預設值 | 有(0/null/false) | 無(必須顯式初始化) |
| 執行緒安全 | 非安全(需同步) | 天然安全(線程私有) |
總結:記憶體模型心智圖
棧 (Thread Stack) 堆 (Heap)
├── main() 棧幀 ├── Test 物件
│ ├── date=9 (int) │ └── (無實例欄位)
│ ├── test ────────────────────────┼──► (物件頭 + 實例資料)
│ └── d1 ──────────────────────────┼──► BirthDate 物件
│ change() 棧幀 │ ├── day=7
│ └── i=9 (參數) │ ├── month=7
│ │ └── year=1970
│ │
常量池 (String Pool) │
├── "china" ◄── s1, s2, s3 │
└── (其他字面量) │
掌握這張圖,您就擁有了診斷記憶體洩漏、優化物件分配、解讀 GC 日誌的核心視角。
- 上一篇文章: Java 實現 .NET DataTable 功能系列(一):架構設計與 JUnit 整合測試
- 下一篇文章: Hibernate 中使用 UNION 查詢:繞過 HQL 限制的 Native SQL 實戰方案
Copyright ©
程式碼之家 All Rights Reserved