面試重災區——JVM記憶體結構和GC
阿新 • • 發佈:2020-11-10
## JVM介紹
### 1. JVM的體系架構(記憶體模型)
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200413210115.png)
綠色的為執行緒私有,橘色的為執行緒共有
### 2. 類載入器
負責將`.class`檔案載入到記憶體中,並且將該檔案中的資料結構轉換為方法區中的資料結構,生成一個`Class`物件
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200413210951.png)
#### 2.1 類載入器分類
- 自啟動類載入器。`Bootstrap ClassLoader`類載入器。負責載入`jdk`自帶的包。
- `%JAVA_HOME%/lib/rt.jar%`即JDK原始碼
- 使用`C++`編寫
- 在程式中直接獲取被該載入器載入的類的類載入器會出現`null`
- 擴充套件類載入器.`Extension ClassLoader`。負責載入`jdk`擴充套件的包
- 便於未來擴充套件
- `%JAVA_HOME/lib/ext/*.jar%`
- 應用類載入器或系統類載入器。`AppClassLoader或SystemClassLOader`
- 用於載入自定義類的載入器
- `CLASSPATH`路徑下
- 自定義類載入器
- 通過實現`ClassLoader`抽象類實現
#### 2.2 雙親委派機制
當應用類載入器獲取到一個類載入的請求的時候,不會立即處理這個類載入請求,而是將這個請求委派給他的父載入器載入,如果這個父載入器不能夠處理這個類載入請求,便將之傳遞給子載入器。一級一級傳遞指導可以載入該類的類載入器。
該機制又稱**沙盒安全機制**。防止開發者對`JDK`載入做破壞
![](https://gitee.com/onlyzl/blogImage/raw/master/img/雙親委派機制.png)
#### 2.3 打破雙親委派機制
- 自定義類載入器,重寫`loadClass`方法
- 使用執行緒上下文類載入器
#### 2.4 Java虛擬機器的入口檔案
`sun.misc.Launcher`類
### 3. Execution Engine
執行引擎負責執行解釋命令,交給作業系統進行具體的執行
### 4. 本地介面
#### 4.1 native方法
`native`方法指`Java`層面不能處理的操作,只能通過本地介面呼叫本地的函式庫(`C函式庫`)
#### 4.2 Native Interface
一套呼叫函式庫的介面
### 5. 本地方法棧
在載入`native`方法的時候,會將執行的`C`函式庫的方法,放在這個棧區域執行
### 6. 程式計數器
每個執行緒都有程式計數器,主要作用是儲存程式碼指令,就類似於一個執行計劃。
內部維護了多個指標,這些指標指向了方法區中的方法位元組碼。執行引擎從程式計數器中獲取下一次要執行的指令。
由於空間很小,他是當前執行緒執行程式碼的一個行號指示器/
不會引發OOM
### 7. 方法區
供各執行緒共享的執行時記憶體區域,存放了各個類的結構資訊(一個Class物件),包括:欄位,方法,構造方法,執行時常量池。
**雖然JVM規範將方法區描述為堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開**
**主要有:**永久代或者元空間。存在GC
元空間中由於直接使用實體記憶體的影響,所以預設的最大元空間大小為`1/4`實體記憶體大小
### 8. Java棧
主要負責執行各種方法,是執行緒私有的,隨執行緒的消亡而消亡,不存在垃圾回收的問題。八大資料型別和例項引用都是在函式的棧記憶體中分配記憶體的。
預設大小為`512~1024K`,通過`-Xss1024k`引數修改
#### 8.1 棧和佇列資料結構
棧`FILO`:先進後出
佇列`FIFO`:先進先出
#### 8.2 儲存的資料
- 本地變數`Local Variable`。包括方法的形參和返回值
- 棧操作`Operand Stack`。包括各種壓棧和出棧操作
- 棧幀資料`Frame Data`。就相當於一個個方法。在棧空間中,方法被稱為棧幀
#### 8.3 執行流程
棧中執行的單位是棧幀,棧幀就是一個個方法。
- 首先將`main`方法壓棧,成為一個棧幀
- 然後呼叫其他方法,即再次壓棧
- 棧幀中儲存了這個方法的區域性變量表,運算元棧、動態連結、方法出口等
- 棧的大小和JVM的實現有關,通常在`256K~756K`
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200413215821.png)
### 9. 方法區,棧,堆的關係
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200413220431.png)
### 10. Heap 堆
#### 10.1 堆記憶體結構
預設初始大小為實體記憶體的`1/64`,預設最大大小為`1/4`。在實際生產中一般會將這兩個值設定為相同,避免垃圾回收器執行完垃圾回收以後還需要進行空間的擴容計算,浪費資源。
**堆外記憶體:**記憶體物件分配在Java虛擬機器的堆以外的記憶體,這些記憶體直接受作業系統管理(而不是虛擬機器),這樣做的結果就是能夠在一定程度上減少垃圾回收對應用程式造成的影響。使用未公開的Unsafe和NIO包下`ByteBuffer`來建立堆外記憶體。
預設的堆外記憶體大小為,通過`-XX:MaxDirectMemorySize=`執行堆外記憶體的大小
##### 10.1.1 JDK1.7
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200413221251.png)
**在邏輯上劃分為三個區域:**
- 新生區`Young Generation Space`。
- 伊甸區`Eden Space`
- 倖存區`Survivor 0 Space`
- 倖存區`Survivor 1 Space`
- 養老區`Tenure Generation Space`
- 永久區`Permanent Space`(方法區)
**在物理層面劃分為兩個區域:**
- 新生區
- 老年區
###### 10.1.1.1 堆記憶體GC過程
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200413222739.png)
**主要流程有三步:**
- 當`Eden`滿了以後出發一次輕GC(`Minor GC`),沒有死亡的物件,年齡`+1`,存放到`from`區域
- 當`Eden`再次滿了以後再次觸發一次`GC`,沒有死亡的物件放置於`to`區域,然後將`from`區域中沒有死亡的物件全部置於`to`區域,年齡`+1`。之後每一次GC都會出發一次`from`和`to`的交換,**哪個區域是空的那個區域就是`to`**
- 當`survivor`區域滿了以後,再次觸發GC,當存在物件的年齡等於`15`的時候,就會將該物件移入老年區
- `MaxTenuringThreshold`通過這個引數設定當年齡為多少的時候移入
- 老年區滿了以後觸發一次`Full GC`,如果老年區無法再存放物件直接報`OOM`
**注意:每一次GC都會給存活的物件的年齡+1**
##### 10.1.2 JDK1.8
和`1.7`相比,僅僅是將永久代更替為了**元空間**。元空間的存放內建是**實體記憶體**,而不是`JVM`中。
這樣處理,可以使元空間的大小不再受虛擬機器記憶體大小的影響,而是由系統當前可用的空間來控制。
新生區和老年區的大小比例為`1:2`,通過`-XX:NewRatio=n`設定新生代和老年代的比例,n代表老年區所佔的比例。
Eden Space和Survivor Space之間的比例預設為`8:1`,通過`-XX:SurvivorRatio`設定伊甸區和倖存者區的比例
**邏輯層面分層:**
- 新生區`Young Generation Space`
- 伊甸區`Eden Space`
- 倖存區`Survivor 0 Space`
- 倖存區`Survivor 1 Space`
- 老年區`Tenure Generation Space`
- 元空間(方法區)
**物理層面分層:**
- 新生區 **他佔據堆的1/3**
- 老年區 **他佔據堆的2/3**
#### 10.2 堆引數調優
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200414202111.png)
##### 10.2.1 常用堆引數
| 引數 | 作用 |
| ------------------- | ------------------------------------ |
| -Xms | 設定初始堆大小,預設為實體記憶體的1/64 |
| -Xmx | 設定最大堆大小,預設為實體記憶體的1/4 |
| -XX:+PrintGCDetails | 輸出詳細的GC日誌 |
**模擬OOM**
```java
//設定最大堆記憶體為10m
//-Xms10m -Xmx10m -XX:+PrintGCDetails
```
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200418134104.png)
下面我們具體分析一下GC的過程做了什麼,GC日誌怎麼看
**名稱:GC以前佔用->GC之後佔用(總共佔用)**
```java
//GC 分配失敗
GC (Allocation Failure)
[PSYoungGen: 1585K->504K(2560K)] 1585K->664K(9728K), 0.0009663 secs] //[新生代,以前佔用->執行緒佔用(總共空閒)] 堆使用大小->堆現在大小(總大小)
[Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure)
[PSYoungGen: 0K->0K(2560K)]
[ParOldGen: 590K->573K(7168K)] 590K->573K(9728K),
[Metaspace: 3115K->3115K(1056768K)], 0.0049775 secs]
[Times: user=0.00 sys=0.00, real=0.01 secs]
```
### 11. 垃圾回收演算法
#### 11.1 垃圾回收型別
- 普通GC(`minor GC`)發生在新生區的,很頻繁
- 全域性GC`major GC`發生在老年代的垃圾收集動作,出現一次`major GC`經常會伴隨至少一次的`Minor GC`
#### 11.2 垃圾回收演算法分類
##### 11.2.1 引用計數法
**主要思想:**每存在一個物件引用就給這個物件加一,當這個物件的引用為零的時候,便觸發垃圾回收。**一般不使用**
**缺點:**
- 每次新建立物件就需要新增一個計數器,比較浪費
- 迴圈引用較難處理
##### 11.2.2 複製演算法
**主要思想:**將物件直接拷貝一份,放置到其他區域
**優點:**不會產生記憶體碎片
**缺點:**佔用空間比較大
**使用場景:**新生區的複製就是通過複製演算法來執行的。當`Minor Gc`以後,就會倖存的物件複製一份放置到`to`區
##### 11.2.3 標記清除演算法
**主要思想:**從引用根節點遍歷所有的引用,標記出所有需要清理的物件,然後進行清除。**兩步完成**
**缺點:**在進行垃圾回收的時候會打斷整個程式碼的執行。會產生記憶體碎片
##### 11.2.4 標記整理演算法
**主要思想:**和標記清除演算法一樣,最後添加了一個步驟整理,將整理記憶體碎片。**三步完成**
**缺點:**效率低,需要移動物件。
#### 11.3 各大垃圾回收演算法比較
##### 11.3.1 記憶體效率
複製演算法>標記清除法>標記整理法
##### 11.3.2 記憶體整齊度
複製演算法=標記整理法>標記清除法
##### 11.3.3 記憶體利用率
標記整理法=標記清除法>複製演算法
##### 11.3.4 最優演算法
通過場景使用不同的演算法,來達到最優的目的
年輕代:因為其物件存活時間段,物件死亡率高,所以一般使用複製演算法
老年代:區域大,存活率高,一般採用標記清除和標記整理的混合演算法。
**老年代一般是由標記清除或者是標記清除與標記整理的混合實現。以hotspot中的CMS回收器為例,CMS是基於Mark-Sweep實現的,對於對像的回收效率很高,而對於碎片問題,CMS採用基於Mark-Compact演算法的Serial Old回收器做為補償措施:當記憶體回收不佳(碎片導致的Concurrent Mode Failure時),將採用Serial Old執行Full GC以達到對老年代記憶體的整理。**
##### 11.3.5 GCRoots
上面我們提到標記清除演算法的時候,提到了一個名詞,**根節點引用**。那麼什麼叫做根節點引用呢?
根節點引用也成`GCRoots`,他是指垃圾回收演算法進行物件遍歷的根節點。即從這個物件開始往下遍歷,標記需要進行回收的物件。
垃圾回收標記的過程就是:以`GCRoots`物件開始向下搜尋,如果一個物件到`GCRoots`沒有任何的引用鏈相連時,說明此物件不可用。
就是從`GCRoots`進行遍歷,**可以被遍歷到的就不是垃圾,沒有被遍歷到的就是垃圾,判定死亡**
###### 11.3.5.1 可達性物件和不可達性物件
可達性物件是指,在物件鏈路引用的頂層是一個`GCRoot`引用
不可達物件是指,在物件鏈路引用的頂層不是一個`GCRoot`引用
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200418121525.png)
**通俗解釋:**可達性物件就是物件有一個歸屬,這個歸屬有一個術語名稱叫做`GCRoot`,不可達性物件就是這些物件沒有歸屬。
###### 11.3.5.2 什麼引用可以作為GCRoots
- 棧內的區域性變數引用
- 元空間中的靜態屬性引用
- 元空間中的常量引用
- 本地方法棧中`native`修飾的方法
**說白了,就是所有暴露給開發者的引用**
### 12. 垃圾回收器
垃圾回收器是基於`GC`演算法實現的。
主要有四種垃圾回收器,不過具體有七種使用方式
#### 12.1 四種垃圾回收器
##### 12.1.1 序列垃圾回收器(Serial)
單執行緒進行垃圾回收,此時其他的執行緒全部被暫停
通過`-XX:+UseSerialGC`
##### 12.1.2 並行垃圾回收器(Parallel)
多執行緒進行垃圾回收,此時其他的執行緒全部被暫停
##### 12.1.3 併發垃圾回收器(CMS)
GC執行緒和使用者執行緒同時執行
##### 12.1.4 G1垃圾回收器
分割槽垃圾回收。物理上不區分新生區和養老區,將堆記憶體劃分為`1024`個小的`region`,每一個佔據的空間在`2~32M`,每一個`region`都可能是`Eden Space`、`Survivor01 Space`、`Survivor02 Space`和`Old`區。
整體使用了標記整理演算法,區域性使用了複製演算法。通過複製演算法將GC後的物件從一個`region`向另一個`region`遷移,至於造成了記憶體碎片問題,通過整體的標記整理演算法,避免了記憶體碎片的誕生
在進行垃圾回收的時候直接對一個`region`進行回收,儲存下來的物件通過複製演算法複製到`TO`區或者`Old`區。
邏輯上堆有四個區,每一個區的大小不定,按需分配。分為`Eden Space`,`Survivor01 Space`,`Old`和`Humongous`。其中`Humongous`用來存放大物件,一般是連續儲存,當由於連續`region`不足的時候,會觸發`Full GC`清理周圍的`Region`以存放大物件
**G1堆記憶體示意**
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419200009.png)
**G1垃圾回收**
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419200347.png)
**出現大物件,三個region不能存放,進行FullGC**
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419200706.png)
**執行流程**
- 初始標記。**GC多執行緒**,標記`GCRoots`
- 併發標記。**使用者執行緒和GC執行緒同時進行**。GC執行緒遍歷`GCRoots`的所有的物件,進行標記
- 重新標記。修正被併發標記標記的物件,由於使用者程式再次呼叫,而需要取消標記的物件。**GC執行緒**
- 篩選回收。清理被標記的物件。**GC執行緒**
- 使用者執行緒繼續執行
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419202001.png)
###### 12.1.4.1 案例
- 初始標記。是通過一個大物件引發的G1
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419202324.png)
- 併發標記
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419202510.png)
- 重新標記、篩選清理和大物件引發的`Full GC`
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419202602.png)
###### 12.1.4.2 G1常用引數
```java
-XX:+UseG1GC 開啟GC
-XX:G1HeapRegionSize=n : 設定G1區域的大小。值是2的冪,範圍是1M到32M。目標是根據最小的Java堆大小劃分出約2048個區域
-XX:MaxGCPauseMillis=n : 最大停頓時間,這是個軟目標,JVM將盡可能(但不保證)停頓時間小於這個時間
-XX:InitiatingHeapOccupancyPercent=n 堆佔用了多少的時候就觸發GC,預設是45
-XX:ConcGCThreads=n 併發GC使用的執行緒數
-XX:G1ReservePercent=n 設定作為空閒空間的預留記憶體百分比,以降低目標空間溢位的風險,預設值是10%
```
#### 12.2 常用引數
```java
DefNew Default New Generation //序列垃圾回收器,新生代叫法
Tenured Old //序列垃圾回收器,老年代叫法
ParNew Parallel New Generation //新生代並行垃圾回收器,新生代叫法
PSYongGen Parallel Scavenge //新生代和老年代垃圾回收器,叫法
ParOldGen Parallel Old Generation //新生代和老年代垃圾回收器,叫法
```
#### 12.3 新生代垃圾回收器
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419163703.png)
上圖顯示的是新生區和老年區可以使用垃圾回收器的所有種類,我們一個一個來說明
##### 12.3.1 序列GC(Serial/Serial Coping)
**新生代**使用`Serial Coping`垃圾回收器使用**複製演算法**
**老年區**預設使用`Serial Old`垃圾回收器,使用**標記清除演算法和標記整理演算法**
通過`-XX:+UseSerialGC`設定
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419171045.png)
##### 12.3.2 並行GC(ParNew)
**新生區**使用`ParNew`垃圾回收器,使用複製演算法
**老年區**使用`Serial Old`垃圾回收器(不推薦這樣使用),使用標記清除演算法和標記整理演算法
通過`-XX:+UseParNewGC`啟動
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419171538.png)
##### 12.3.3 並行回收GC(Parallel/Parallel Scavenge)
**新生代**使用並行垃圾回收
**老年代**使用並行垃圾回收。Java1.8中預設使用的垃圾回收器
**一個問題:Parallel和Parallel Scavenge收集器的區別?**
`Parallel Scavenge`收集器類似於`ParNew`也是一個新生代的垃圾收集器,使用了複製演算法,也是一個並行的多執行緒的垃圾收集器,俗稱吞吐量優先收集器。
`parallel Scavenge`是一種自適應的收集器,虛擬機器會根據當前系統執行情況收集效能監控資訊,動態調整這些引數以提供最合適的提頓時間或者最大吞吐量
**他關注的點是:**
可控制的吞吐量。吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間),
同時,當新生代選擇為`Parallel Scavenge`的時候,會預設啟用老年區使用並行垃圾回收
通過`-XX:UseParallelGC或者-XX:UseParallelOldGC`兩者會互相啟用
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419172007.png)
**`-XX:ParallelGCThreads=n`表示啟動多少個GC執行緒**
`cpu>8時 N=5或者8`
`cpu<8時 N=實際個數`
#### 12.4 老年代垃圾回收器
##### 12.4.1 序列垃圾回收器(Serial Old/Serial MSC)
`Serial Old`是`Serial`垃圾收集器老年代版本,是一個單執行緒的收集器,使用**標記整理演算法**,執行在`Client`中的年老代垃圾回收演算法
與新生代的`Serial GC`相關聯
##### 12.4.2 並行回收(Parallel Old/Parallel MSC)
`Parallel Old/`採用**標記整理演算法**實現
與新生代的`Parallel Scavenge GC`相關聯
##### 12.4.3 併發標記清除GC
`CMS`收集器(`Concurrent Mark Sweep`併發標記清除):一種以獲取最短回收停頓時間為目標的收集器
適合應用在網際網路站或者`B/S`系統的伺服器上,重視伺服器的響應速度
`CMS`非常適合堆記憶體大、`CPU`核數多的服務端應用,也是`G1`出現之前大型應用的首選收集器
**標記的時候,GC執行緒執行;清除的時候和使用者執行緒一起執行**
通過`-XX:+UseConcMarkSweepGC`指令開啟
配合**新生區的`pallellal New GC`回收器使用**
**當CMS由於CPU壓力太大無法使用的時候會使用`SerialGC`作為備用收集器**
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419185937.png)
###### 12.4.3.1 CMS執行過程
- 初始標記(`CMS initial mark`)。遍歷尋找到所有的`GCRoots`。`GC`執行緒執行,使用者執行緒暫停
- 併發標記(`CMS concurrent mark`)和使用者執行緒一起遍歷`GCRoots`,標記需要清除的物件
- 重新標記(`CMS remark`)。修正標記期間,對因使用者程式繼續執行而不需要進行回收的物件進行修正
- 併發清除(`CMS concurrent sweep`)和使用者執行緒一起清除所有標記的物件
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419190511.png)
###### 12.4.3.2 優缺點
**優點:**
- 併發收集低停頓
**缺點:**
- 併發執行,對CPU資源壓力大
- 採用標記清除演算法會導致大量的記憶體碎片
#### 12.5 垃圾回收器小結
| 引數(-XX:+……) | 新生代垃圾回收器 | 新生代演算法 | 老年代垃圾回收器 | 老年代演算法 |
| ------------------ | -------------------- | ---------- | ------------------ | ---------- |
| UseSerialGC | SerialGC | 複製演算法 | Serial Old GC | 標整 |
| UseParNewGC | Parallel New GC | 複製演算法 | Serial Old GC | 標整 |
| UseParllelGC | Parallel Scavenge GC | 複製演算法 | Parallel GC | 標整 |
| UseConcMarkSweepGC | Parallel New GC | 複製演算法 | CMS和Serial Old GC | 標清 |
| UseG1GC | 整體標整 | | 區域性複製 | |
**垃圾回收演算法通用邏輯**
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200419191717.png)
#### 12.6 CMS和G1的區別
- G1不會引發記憶體碎片
- G1對記憶體的精準控制,可以精準的去收集垃圾。**根據設定的GC處理時間去收集垃圾最多的區域**
### 13. JMM
java記憶體模型。是一種規範。
**執行緒在操作變數的時候,首先從實體記憶體中複製一份到自己的工作記憶體中(棧記憶體),更新以後再寫入實體記憶體中**
**特點:**
- 原子性
- 可見性
- 有序性
![](https://gitee.com/onlyzl/blogImage/raw/master/img/20200414212439.png)
> 更多原創文章和學習教程請關注 同名公眾號@Ma