JVM筆記-執行時記憶體區域劃分
阿新 • • 發佈:2020-03-17
## 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) {