1. 程式人生 > >JVM筆記-執行時記憶體區域劃分

JVM筆記-執行時記憶體區域劃分

## 1. 概述 Java 虛擬機器在執行 Java 程式的過程中會把它管理的記憶體劃分為若干個不同的資料區域。它們各有用途,有些隨著虛擬機器程序的啟動一直存在(堆、方法區),有些則隨著使用者執行緒的啟動和結束而建立和銷燬(程式計數器、虛擬機器棧、本地方法棧)。 《Java 虛擬機器規範》中規定 Java 虛擬機器管理的記憶體包括以下幾個區域: 下面簡要分析各個區域的特點。 ## 2. JVM 執行時記憶體區域 ### 2.1 程式計數器 程式計數器(Program Counter Register),可以看做當前執行緒所執行的位元組碼的行號指示器(其實就是記錄程式碼執行到了哪裡)。特點如下: - 執行緒私有; - 佔用記憶體空間較小; - 若執行緒執行的是 Java 方法,記錄的是虛擬機器位元組碼指令地址;若執行的是本地(Native)方法,則為空(Undefined); - 該區域是唯一一個在《Java 虛擬機器規範》中規定無任何 OutOfMemoryError 的區域。 > 主要作用:記錄執行緒執行到了哪裡。 ### 2.2 Java 虛擬機器棧 Java 虛擬機器棧(Java Virtual Machine Stacks):Java 方法執行的執行緒記憶體模型。 每個方法被執行時,虛擬機器棧都會建立一個棧幀(Stack Frame)用於儲存區域性變量表、運算元棧、動態連線、方法出口等資訊。每個方法從被呼叫直至執行完畢的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。其中區域性變量表包括: - Java 虛擬機器基本資料型別(8 種) - 物件引用(reference 型別,可能是一個指向物件起始地址的指標) - returnAddress 這些資料型別在區域性變量表中的儲存空間以區域性變數槽(Slot)表示,其中 long 和 double 佔用兩個槽,其他型別佔用一個槽。區域性變量表所需記憶體空間在編譯期完成分配,當進入一個方法時,該方法需要在棧幀中分配多大的區域性變數空間是完全確定的,執行期間不會改變其大小。 虛擬機器棧的特點: - 執行緒私有; - 生命週期與執行緒相同; - 兩類異常 - - 執行緒請求的棧深度大於虛擬機器所允許的深度時丟擲 StackOverflowError 異常; - 棧擴充套件時無法申請到足夠的記憶體時丟擲 OutOfMemoryError 異常。 > 主要目的:Java 方法執行的執行緒記憶體模型。 ### 2.3 本地方法棧 本地方法棧(Native Method Stacks)與 Java 虛擬機器棧作用類似。二者區別: - Java 虛擬機器棧為 JVM 執行 Java 方法(位元組碼)服務; - 本地方法棧為 JVM 使用到的本地(Native)方法服務。 異常與 Java 虛擬機器棧相同。 > 主要目的:Native 方法執行的執行緒記憶體模型。 ### 2.4 Java 堆 對多數應用來說,Java 堆(Java Heap)是 JVM 管理的記憶體中最大的一塊。 唯一目的:存放物件例項(【幾乎所有】的物件例項都在這裡分配記憶體)。 > 《Java 虛擬機器規範》描述:所有物件例項及陣列都應在堆上分配。 > > 而從實現角度看,由於即使編譯技術(尤其是逃逸分析技術的日漸強大),"棧上分配"等手段使得物件並非完全在堆上分配。 特點: - 執行緒共享 - 虛擬機器啟動時建立 > PS: "新生代"、"老年代"、"Eden 區"等一系列對堆的區域劃分,只是部分垃圾收集器的一些共性或設計風格,而非虛擬機器的固有記憶體佈局,更非《Java 虛擬機器規範》的劃分。 > > 將 Java 堆細分的目的只是為了更好地回收記憶體,或者更快地分配記憶體。 ### 2.5 方法區 方法區(Method Area):用於儲存已被虛擬機器載入的型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等資料,該區域也是執行緒共享的。又稱"非堆"。 與方法區聯絡密切的一個概念是"永久代",下面簡要介紹。 - 永久代 "永久代(Permanent Generation)",可以理解為 JDK 1.8 之前 HotSpot 虛擬機器對《Java 虛擬機器規範》中"方法區"的實現。從 JDK 1.6、1.7 到 1.8+,HotSpot 虛擬機器的執行時資料區變遷示意圖如下: HotSpot VM JDK 1.6 的執行時資料區示意圖如下:
JDK 1.7 中,將 1.6 中永久代的字串常量池和靜態變數等移到了堆中,如下(虛線框表示已移除): 而到了 JDK 1.8,則完全廢棄了"永久代",改用了在本地記憶體中實現的"元空間(Metaspace)",將 JDK 1.7 中永久代剩餘的部分(主要是型別資訊)移到了元空間,如下(虛線框表示已移除): 從上面幾張圖可以看出永久代和元空間的主要區別有以下兩點: 1. 儲存位置不同 2. 1. 永久代是 JVM 記憶體的一部分,元空間在本地記憶體中(JVM 記憶體之外); 2. 永久代使用不當可能導致 OOM,元空間一般不會。 3. 儲存內容不同:元空間儲存的是「型別資訊」(即類的元資訊),而永久代除了型別資訊,還包括「字串常量池」和「靜態變數」等(可以理解為元空間是永久代拆分出來的一部分)。 那麼問題來了:為什麼要把永久代替換為元空間呢? 原因大概有以下幾點: 1. Oracle 收購了兩種 JVM:HotSpot VM 和 JRockit VM,並且想要將它們整合,但二者方法區實現差異較大; 2. 字串存在永久代中,容易出現效能問題和 OOM; 3. 類及方法的資訊大小較難確定,永久代大小難以確定:太小易導致永久代溢位,太大則易導致老年代溢位(JVM 記憶體是有限的,此消彼長); 4. 永久代會為垃圾回收帶來不必要的複雜度,且回收效率較低("價效比"低)。 ### 2.6 執行時常量池 執行時常量池(Runtime Constant Pool)是方法區的一部分。 Class 檔案中除了有類的版本、欄位、方法、介面等描述外資訊,還有一項資訊是常量池表(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。 相比於 Class 檔案常量池的一個重要特性是「動態性」,執行期間也可以將新的常量放入池中(例如 String 類的 intern() 方法)。 - 可能產生的異常:OutOfMemoryError。 ### 2.7 直接記憶體 直接記憶體(Direct Memory)並非虛擬機器執行時資料區的一部分,也非《Java 虛擬機器規範》定義的記憶體區域。但該部分記憶體被頻繁使用(例如 NIO),而且可能導致 OutOfMemoryError。 ## 3. OOM異常實踐 ### 3.0 作業系統及 JDK 版本 - 作業系統:macOS Mojave 10.14.5 - JDK 1.8 ```bash $ java -version java version "1.8.0_191" Java(TM) SE Runtime Environment (build 1.8.0_191-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode) ``` - JDK 1.7 ```bash $ java -version java version "1.7.0_80" Java(TM) SE Runtime Environment (build 1.7.0_80-b15) Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode) ``` ### 3.1 Java 堆溢位 - 示例程式碼(JDK 1.8) ```java public class HeapOOM { public static void main(String[] args) {