JVM 內存三大區域
| 區域 | 存儲內容 | 線程共享性 | 生命週期 |
| 堆 | 所有對象實例、數組對象 | 全局共享 | GC 管理,進程存續 |
| 棧 | 基本類型變量、對象引用、方法幀、操作數棧 | 線程私有 | 隨線程/方法創建銷毀 |
| 方法區 | 類元數據、常量池、static 字段、JIT 編譯代碼 | 全局共享 | 進程存續(元空間) |
對象創建與訪問全過程
Sample test1 = new Sample("測試1");
步驟拆解
- 類加載:JVM 在方法區查找 Sample 類元數據,無則加載(讀取 .class → 解析 → 存入方法區)
- 堆分配:在堆中劃分內存區域,存儲對象實例數據(字段值)+ 指向方法區類元數據的指針(Klass Pointer)
- 棧分配:當前線程棧幀局部變量表槽位存儲引用變量
test1,指向堆中對象地址
- 初始化:執行構造器,字段賦值(基本類型默認值 → 顯式賦值 → 構造器代碼)
- 方法調用:
test1.printName() → 棧幀入棧 → 根據引用定位堆對象 → 根據 Klass Pointer 定位方法區字節碼 → 執行
基本類型 vs 引用類型存儲差異
基本類型(8 種)
- 字面值直接存儲在棧(局部變量)或堆(對象字段)
- 大小固定、生命週期可知、訪問速度最快
- 值語義:
int a=3; int b=3; 棧中共享字面值 3;a=4 不影響 b
包裝類/引用類型
- 對象實體在堆,引用在棧
Integer a=3; Integer b=3; 自動裝箱可能複用緩存池(-128~127),但 new Integer(3) 必創新對象
String 特殊:字面值進常量池,new String() 強制堆分配
經典案例圖解
public class AppMain {
public static void main(String[] args) {
Sample test1 = new Sample("測試1");
Sample test2 = new Sample("測試2");
test1.printName();
test2.printName();
}
}
class Sample {
private String name;
public Sample(String name) { this.name = name; }
public void printName() { System.out.println(name); }
}
內存圖景
- 方法區:AppMain.class、Sample.class、字符串常量 "測試1"、"測試2"
- 堆:Sample#1 (name→"測試1")、Sample#2 (name→"測試2")
- 主線程棧:main() 幀 → 局部變量 test1→Sample#1, test2→Sample#2
- printName() 調用:新建棧幀,this 指向對應 Sample 實例
棧與堆性能對比
| 維度 | 棧 | 堆 |
| 分配速度 | 指針移動,極快 | TLAB/指針碰撞/空闲列表,較慢 |
| 訪問速度 | 直接偏移量,最快 | 引用解引用,間接訪問 |
| 靈活性 | 大小/生命週期必須編譯期確定 | 動態分配,GC 自動回收 |
| 共享性 | 線程私有,不可共享 | 全局共享,需同步控制 |
String 對象特殊性(預告下篇重點)
- 字面值進常量池(方法區/堆,版本差異),
intern() 手動入池
+ 拼接編譯期優化為 StringBuilder,運行期 new StringBuilder().append().toString()
- 不可變性導致頻繁創建臨時對象,性能敏感場景用 StringBuilder/StringBuffer
總結
Java 內存模型清晰分工:方法區存類、堆存對象、棧存引用與基本類型。理解「引用在棧、對象在堆」是排查 OOM、內存洩漏、逃逸分析的基礎。現代 JVM (JDK 8+) 永久代→元空間,字符串常量池遷移至堆,但核心邏輯不變。建議配合 -XX:+PrintGCDetails、JVisualArth、Arthas 等工具實戰觀察。