本文将介紹了 Java 虛拟機記憶體的各個區域以及這些區域的作用、服務對象和其中可能出現的異常等。
JVM 運作時資料區域
Java 虛拟機在執行 Java 程式的過程中會把它所管理的記憶體劃分為五個不同的資料區域(如圖所示)。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yMjZGNwMzN0ETZhVTMzADM3Q2YkBDM4UzM1ImY4Q2N48CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
- 紅色邊框的是由所有線程共享的資料區
- 藍色邊框的是線程隔離的資料區
除了程式計數器之外,其他四個區域都可能會出現
OutOfMemoryError
異常。
程式計數器
- 程式計數器是一塊較小的記憶體空間
- 是目前線程所執行的位元組碼的行号顯示器
- 位元組碼解釋器就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令的
- 執行 Java 方法和執行 Native 方法的差別:
- 執行 Java 方法時,計數器記錄虛拟機正在執行的位元組碼指令的位址
- 執行 Native 方法時,無記錄,也就是計數器的值為空(Undefined)
程式計數器是唯一一個不會出現 OOM 的區域。
Java 虛拟機棧
- 每個 Java 方法被執行的時候,Java 虛拟機都會同步建立一個棧幀用于存儲局部變量表、操作數棧、動态連接配接、方法出口等資訊
- 每個方法被調用一直到執行完畢的過程,都對應着一個棧幀在 Java 虛拟機棧中從入棧到出棧的過程
- Java 虛拟機棧服務于 Java 方法
- 可能出現的異常:
-
:線程請求的棧深度 大于 Java 虛拟機所允許的深度時StackOverflowError
-
:在 Java 虛拟機棧容量可動态擴充的情況下,當棧擴充時無法申請到足夠的記憶體時OutOfMemoryError
-
- 虛拟機參數設定:
-Xss
本地方法棧
- 與 Java 虛拟機棧發揮的作用類似,差別在于本地方法棧服務于 Native 方法。
- 可能出現的異常:與 Java 虛拟機棧一樣。
Java 堆
- 唯一目的:存放對象執行個體
- 垃圾回收器管理的記憶體區域
- 可以處于實體上不連續的記憶體空間中,但是在邏輯上應該是連續的
- 可能出現的異常:
-
:Java 堆中沒有記憶體完成執行個體配置設定,并且堆也無法再擴充時OutOfMemoryError
-
- 虛拟機參數設定:
- 最大值:
-Xmx
- 最小值:
-Xms
- 兩個參數設定成相同值時可避免堆自動擴充
- 最大值:
方法區
- 用于存儲已被 Java 虛拟機加載的類型資訊、常量、靜态變量、即時編譯器編譯後的代碼緩存等資料
- 可以選擇不實作垃圾收集,換句話說垃圾收集行為在這個區域非常少見,但是對于經常動态性生成大量 Class 的應用,如 Spring 等,需要特别注意類的回收情況
- 可能出現的異常:
-
:方法區無法滿足新的記憶體配置設定需求時OutOfMemoryError
-
運作時常量池
- 運作時常量池是方法區的一部分
- Class 檔案除了有類的版本、字段、方法、接口等描述資訊外,還有一項是常量池表,用于存放編譯期生成的各種字面量(就是代碼中定義的 static final 常量)和符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中
- 可能出現的異常:
-
:常量池無法再申請到記憶體時OutOfMemoryError
-
直接記憶體
- 不屬于 Java 虛拟機運作時資料區域的一部分,放到這裡講是因為這部分記憶體在使用的時候也可能導緻
異常的出現:各個記憶體區域總和大于實體記憶體限制(包括實體的和作業系統級的限制),進而導緻動态擴充時出現OutOfMemoryError
異常OutOfMemoryError
- JDK1.4 的 NIO 類可以使用 Native 函數庫直接配置設定堆外記憶體,然後通過一個存儲在 Java 堆裡面的 DirectByteBuffer 對象作為這塊記憶體的引用進行操作,好處是避免了在 Java 堆和 Native 堆中來回複制資料,能在一些場景中提高性能
- 虛拟機參數設定:
-XX:MaxDirectMemorySize
- 預設等于 Java 堆最大值,即
指定的值-Xmx
- 預設等于 Java 堆最大值,即
HotSpot 虛拟機堆中的對象
介紹完 Java 虛拟機的運作時資料區域後,我們大緻了解的 Java 虛拟機記憶體模型的概況。這一小節将介紹 HotSpot 虛拟機在 Java 堆中對象配置設定、布局和通路的全過程。
對象的建立(new)
當虛拟機遇到 new 指令時:
- 檢查這個指令的參數是否能在常量池中定位到一個類的符号引用,并檢查這個符号引用代表的類是否已經被加載、解析和初始化過。如果沒有,則先把這個類加載進記憶體
- 類加載檢查通過後,虛拟機将為這個新的對象配置設定記憶體,記憶體的大小在類加載完成後确定
- 在 Java 堆中為新對象配置設定可用記憶體
- 記憶體配置設定完成後,虛拟機将配置設定到的記憶體空間都初始化為零值
- 虛拟機設定對象頭中的資料
- 此時,從虛拟機的角度看,對象已經建立好了,但從 Java 程式角度看,對象建立才剛剛開始,構造函數還沒有執行
第 3 步中,為對象配置設定可用記憶體時,會涉及兩個問題:
1. 記憶體配置設定方式
- 指針碰撞(Java 堆中的記憶體是絕對規整的)
- 所有被使用過的記憶體放在一邊,沒有被使用過的記憶體放在另一邊,中間放一個指針,作為分界點的訓示器,那所配置設定的記憶體就僅僅是把那個指針向空閑記憶體空間方向挪一段與對象大小相等的距離
- 空閑清單(Java 堆中被使用的記憶體和空閑記憶體互相交錯在一起)
- Java 虛拟機需要維護一個清單,記錄上哪些記憶體塊是可用的,在配置設定的時候從清單中找一塊大小足夠大的空間劃分給對象執行個體
2. 在并發情況下虛拟機建立對象也并不是線程安全的,可能出現正在給對象 A 配置設定記憶體,指針還沒來得及修改,對象 B 又同時使用了原來的指針來配置設定記憶體的情況
- 對配置設定記憶體空間的動作進行同步處理( 采用 CAS 配上失敗重試的方式保證更新操作的原子性 )
- 把記憶體配置設定的動作按照線程劃分在不同的空間中進行(每個線程在 Java 堆中預先配置設定一塊小記憶體(這個小記憶體稱為**本地線程配置設定緩沖區**),哪個線程要配置設定記憶體,就在哪個線程的本地線程配置設定緩沖區中配置設定,隻有這個本地線程配置設定緩沖區配置設定完了,配置設定新的本地線程配置設定緩沖區才需要同步鎖定)
- 虛拟機參數設定:`-XX:+/-UseTLAB`
對象的記憶體布局
在 HotSpot 虛拟機中,對象在堆記憶體中的存儲布局分為三部分:
- 對象頭(包括兩類資訊)
- 用于存儲對象自身的運作時資料,如 HashCode、GC 分代年齡、鎖狀态标志、線程持有的鎖、偏向線程 ID、偏向時間戳等
- 類型指針,即對象指向它的類型中繼資料的指針,虛拟機通過這個指針來确定該對象是哪個類的執行個體。
- 執行個體資料(存儲我們在程式代碼中定義的各種類型的字段内容)
- 這部分資料受到虛拟機配置設定政策參數(
)和字段在代碼中定義順序的影響-XX:FieldsAllocationStyle
- 這部分資料受到虛拟機配置設定政策參數(
- 對齊填充(沒有實際意義,起到占位符的作用)
對象的通路定位
我們建立對象之後自然是要使用對象,Java 程式會通過棧上的 reference 資料來操作堆上的具體對象(reference 是一個指向對象的引用)。
主流的對象通路方式有兩種:
- 通過句柄通路(reference 中存儲的是穩定的句柄位址,在對象被移動的時候隻會改變句柄中的執行個體資料指針,而 reference 本身不需要被修改)
- 通過直接指針通路(速度快得一批,節省了一次指針定位的時間開銷)
本文小結
本文從概念上介紹了 Java 虛拟機記憶體的各個區域以及這些區域的作用、服務對象和其中可能出現的異常等,還介紹了虛拟機建立對象(new)的過程、對象的記憶體布局和如何通路對象。