運行時數據區 1. 程式計數器 (Program Counter) 每個線程獨占自己的程式計數器。如果當前執行的方式不是native的,那程式計數器保存JVM正在執行的位元組碼指令的地址,如果是native的,那程式計數器的值是undefined。 &e ...
運行時數據區
1.程式計數器(Program Counter)
每個線程獨占自己的程式計數器。如果當前執行的方式不是native的,那程式計數器保存JVM正在執行的位元組碼指令的地址,如果是native的,那程式計數器的值是undefined。
此記憶體區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。
2.Java虛擬機棧
每個線程獨占自己的Java虛擬機棧,這個棧與線程同時創建,用於存儲棧幀。
Java虛擬機棧所使用的記憶體不需要保證連續的。
- StackOverflowError:線程請求分配的棧容量超過Java虛擬機棧允許的最大容量。
OutOfMemoryError:如果虛擬機棧可以動態擴展,在嘗試擴展的時候無法申請到足夠的記憶體,或者在創建新的線程時沒有足夠的記憶體去創建對應的虛擬機棧。
3.Java堆
Java堆可供各個線程共用,是給對象分配記憶體的區域,也是需要自動回收垃圾的區域。
堆的容量可以是固定的,也可以隨著程式執行的需求動態擴展,併在不需要時自動收縮。Java堆所使用的記憶體不需要保證是連續的。- OutOfMemoryError:實際所需的堆超過了自動記憶體管理系統能提供的最大容量。
4.方法區
方法區可供各個線程共用。它存儲每一個類的結構信息,例如:運行時常量池、欄位和方法數據、構造函數和不同方法的位元組碼內容,還包括一些在類、實例、介面初始化時用到的特殊方法。
方法區是Java堆的邏輯組成部分,虛擬機可以在這個區域不實現垃圾收集與壓縮。JVM規範也不限定實現方法區的記憶體位置和編譯代碼的管理策略。
方法區容量可以是固定的,也可以隨著程式執行的需求動態擴展,併在不需要時自動收縮。方法區所使用的記憶體不需要保證是連續的。 OutOfMemoryError:方法區的記憶體空間不能滿足記憶體分配請求。
關於永久代和元空間
JDK 8引入了元空間的概念,它和JDK 8之前的永久代都是方法區的實現,區別是,永久代使用Java虛擬機記憶體,而元空間使用本地記憶體,將類的元數據移至元空間,而字元串和類靜態變數移至Java堆。
引入元空間的主要原因有兩個
- 永久代記憶體容易發生記憶體泄露,即
java.lang.OutOfMemoryError: PermGen
- 移除永久代可以促進HotSpot JVM與 JRockit VM的融合,因為JRockit沒有永久代。
5.運行時常量池
運行時常量池是class文件中每一個類或介面的常量池表的運行時表示形式。
運行時常量池在方法區中分配,在載入類和介面到虛擬機後,就創建對應的運行時常量池。 - OutOfMemoryError:方法區的記憶體空間不能滿足記憶體分配請求。
6.本地方法棧
JVM用本地方法棧來支持native方法。如果需要支持native方法,這個棧與線程同時創建。 - StackOverflowError:線程請求分配的棧容量超過本地方法棧允許的最大容量。
- OutOfMemoryError:如果本地方法棧可以動態擴展,在嘗試擴展的時候無法申請到足夠的記憶體,或者在創建新的線程時沒有足夠的記憶體去創建對應的本地方法棧。
7.直接記憶體
不是Java虛擬機規範定義的記憶體,不屬於運行時數據區,JDK NIO中基於通道與緩衝的I/O方式,可以直接分配堆外記憶體。直接記憶體受本機總記憶體的限制。 OutOfMemoryError:動態擴展導致總記憶體超過物理記憶體時。
分配記憶體的兩種方式
- 指針碰撞:記憶體是絕對規整的,用過的記憶體和空閑的記憶體分別放在堆的兩邊,中間放著一個指針作為分界點的指示器,分配記憶體就是指針往空閑區域移動與對象大小相等的距離。
- 空閑列表:記憶體是不規整的,虛擬機維護一個列表,記錄哪些記憶體是可以使用的,分配記憶體就是從列表中找到一塊足夠大的記憶體劃分給對象實例。
選擇哪種分配方式由Java堆記憶體是否規整來決定,而Java堆是否規整又由所用的垃圾收集器是否帶有壓縮整理功能決定。因此,Serial、ParNew等帶壓縮過程的收集器採用指針碰撞,而CMS這種基於Mark-Swap演算法的收集器採用空閑列表。
如何保障分配記憶體時的線程安全性?
- 使用CAS技術保證操作的原子性。
- 每個線程在堆中預先分配一小塊記憶體稱本地線程分配緩衝(TLAB)。哪個線程要分配記憶體,就在哪個線程的TLAB上分配。當TLAB用完,分配新的TLAB時加鎖。
對象的記憶體佈局和訪問
Java虛擬機在堆中給對象分配記憶體,對象在記憶體中的佈局分為三塊
- 對象頭:存儲了Mark Word、類型指針,如果是數組對象,還記錄了數據長度。
- 實例數據:對象的真實數據。
- 對齊填充:由於對象大小必須是8位元組的整數倍,所以需要占位符來對其填充。
主流的對象訪問方式
- 使用句柄訪問:Java堆中劃分一塊記憶體作為句柄池,引用類型指向對象的句柄地址,句柄中包含對象實例數據與類型數據的地址。
- 使用直接指針訪問:引用類型指向對象地址,堆中存儲訪問類型數據的信息。
Hotspot虛擬機使用直接指針的方式來訪問對象。節省了一次指針定位的時間開銷。
如下圖,展示了對象在記憶體的佈局和訪問方式。
參考資料:《深入理解Java虛擬機(第二版)》《Java虛擬機規範(Java SE 8版)》