Java 虛擬機(JVM)是 Java 程序運行的核心環境,它通過內存管理為程序提供高效的執行支持。JVM 在運行時將內存劃分為多個區域,每個區域都有特定的作用和生命周期。本文將詳細介紹 JVM 的運行時數據區域及其功能,并探討與內存相關的常見問題,如內存溢出(OOM)和棧溢出(SOF)。
JVM 運行時數據區域
JVM 將其管理的內存劃分為若干區域,包括線程私有和線程共享兩類。以下是主要區域的概覽:
1. 程序計數器(Program Counter Register)
- 作用:線程私有的小型內存區域,記錄當前線程執行的字節碼指令地址,用于控制程序流程(如分支、循環、跳轉等)。
- 特點:
- 若執行 Java 方法,則記錄字節碼地址;若執行 Native 方法,則為空(Undefined)。
- 是唯一不會拋出
OutOfMemoryError
的區域。
- 生命周期:隨線程創建而生,隨線程結束而滅。
2. Java 虛擬機棧(Java Virtual Machine Stacks)
- 作用:線程私有,管理 Java 方法的執行。每個方法調用對應一個棧幀(Stack Frame),包含局部變量表、操作數棧、動態連接和方法返回地址。
- 棧幀結構:
- 局部變量表:存儲方法參數和局部變量。
- 操作數棧:存放中間計算結果和臨時變量。
- 動態連接:支持方法調用時的符號引用解析。
- 方法返回地址:記錄方法返回位置。
- 異常:
- 棧深度超限:
StackOverflowError
。 - 內存不足:
OutOfMemoryError
。
- 棧深度超限:
- 調整:通過
-Xss
參數設置棧大小。
3. 本地方法棧(Native Method Stack)
- 作用:線程私有,為 Native 方法服務,功能與虛擬機棧類似。
- 異常:同樣可能拋出
StackOverflowError
和OutOfMemoryError
。
4. Java 堆(Java Heap)
- 作用:線程共享,存儲對象實例,是垃圾收集器(GC)的主要管理區域。
- 分代結構:
- 新生代:包括 Eden 和 Survivor(S0、S1),存放新對象。
- 老年代:存放長期存活的對象。
- 元空間(JDK 8 后取代永久代):使用本地內存。
- 年齡限制:對象年齡記錄在對象頭,最大值為 15(4 位二進制),由
-XX:MaxTenuringThreshold
設置。 - 異常:內存不足時拋出
OutOfMemoryError
。 - 調整:通過
-Xms
(初始值)和-Xmx
(最大值)配置堆大小。
5. 方法區(Method Area)
- 作用:線程共享,存儲類信息、常量、靜態變量和編譯后的代碼。
- 演變:
- JDK 7 前:稱為永久代(PermGen),通過
-XX:MaxPermSize
設置。 - JDK 8 后:改為元空間(Metaspace),通過
-XX:MaxMetaspaceSize
設置,使用本地內存。
- JDK 7 前:稱為永久代(PermGen),通過
- 異常:擴展失敗拋出
OutOfMemoryError
。
6. 運行時常量池(Runtime Constant Pool)
- 作用:方法區的一部分,存儲 Class 文件中的字面量(如字符串常量)和符號引用。
- 特點:具備動態性,運行時可通過
String.intern()
添加常量。 - 異常:內存不足拋出
OutOfMemoryError
。
7. 直接內存(Direct Memory)
- 作用:非 JVM 規范定義區域,通過 NIO(如
DirectByteBuffer
)直接分配堆外內存。 - 調整:通過
-XX:MaxDirectMemorySize
設置,默認與-Xmx
一致。 - 異常:分配失敗拋出
OutOfMemoryError
。
內存區域對比
區域 | 作用范圍 | 可能異常 |
---|---|---|
程序計數器 | 線程私有 | 無 |
虛擬機棧 | 線程私有 | StackOverflowError , OutOfMemoryError |
本地方法棧 | 線程私有 | StackOverflowError , OutOfMemoryError |
Java 堆 | 線程共享 | OutOfMemoryError |
方法區 | 線程共享 | OutOfMemoryError |
運行時常量池 | 線程共享 | OutOfMemoryError |
直接內存 | 非運行時區 | OutOfMemoryError |
對象在 JVM 中的生命周期
1. 對象創建
- 步驟:
- 遇到
new
指令,檢查常量池中類的符號引用并加載類。 - 在堆中分配內存(指針碰撞或空閑列表)。
- 初始化對象(調用
<init>()
方法)。
- 遇到
- 內存分配方式:
- 指針碰撞:堆內存規整時使用。
- 空閑列表:堆內存不規整時使用,依賴垃圾收集器是否帶壓縮功能。
- 并發安全:通過 CAS 或 TLAB(線程本地分配緩沖)確保分配安全。
2. 對象內存布局
- 對象頭:
- Mark Word:存儲運行時數據(如哈希碼、GC 年齡、鎖狀態)。
- 類型指針:指向類的元數據。
- 數組長度(僅數組對象):記錄數組大小。
- 實例數據:存儲字段內容。
- 對齊填充:確保內存對齊。
3. 對象訪問
- 句柄訪問:
reference
指向句柄池,穩定但間接。 - 直接指針(HotSpot 默認):
reference
直接指向對象地址,速度更快。
內存溢出與棧溢出
1. OutOfMemoryError (OOM)
除程序計數器外,其他區域都可能因內存不足拋出 OOM。常見場景包括:
- 堆溢出:對象過多,
-Xmx
不足。 - GC 開銷超限:GC 時間超 98% 且回收不足 2%。
- 元空間不足:類加載過多。
- 直接內存溢出:NIO 分配超限。
- 線程創建失敗:系統內存不足以支持新線程。
2. StackOverflowError (SOF)
- 原因:
- 遞歸過深。
- 大量循環或死循環。
- 參數:通過
-Xss
調整棧大小。
示例分析
以下是一個簡單程序的內存分配過程:
public class JVMCase {public final static String MAN_SEX_TYPE = "man"; // 常量池public static String WOMAN_SEX_TYPE = "woman"; // 方法區public static void main(String[] args) {Student stu = new Student(); // 堆中創建對象,棧中存引用stu.setName("nick");JVMCase jvmcase = new JVMCase();print(stu); // 靜態方法入棧jvmcase.sayHello(stu); // 非靜態方法入棧}
}
- 類加載時,靜態變量和常量分配在方法區。
main
執行時,對象在堆中創建,引用存于棧中,方法調用依次入棧。
結語
理解 JVM 內存區域是掌握 Java 性能調優的基礎。從線程私有的程序計數器到共享的 Java 堆和方法區,每個區域各司其職。通過合理配置參數(如 -Xmx
、-Xss
、-XX:MaxMetaspaceSize
),并結合工具(如 jmap、MAT)分析內存問題,可以有效避免 OOM 和 SOF,提升程序穩定性。