文章目錄
內存佈局圖:
一、垃圾回收算法
(1) 標記-清除法
該算法會從每個 GC Roots
出發, 依次標記引用關係的對象, 最後將沒有被標記的對象清除
此算法帶來的後果:
- 帶來大量的空間碎片
若需要分配一個較大連續空間時容易觸發 FGC
就好比在操作系統中對碎片空間的整理
(2) 標記-整理法(Mark-Copy)
此類算法類似計算機的磁盤整理
步驟如下: (不考慮存活對象超過S0
或 S1
)
- 第一次
Minior GC
,Eden
區被清空,Eden
存活對象移至S0
- 第二次
Minior GC
,Eden
區被清空,Eden
存活對象和S0
存活對象移至S1
,S0
區清空 - 之後, 存活對象在
S0
與S1
中交換
Eden
、S0
、S1
內存容量分配
Eden
佔 80%,S0
佔 10%,S1
佔 10%爲什麼這樣區分?
爲了更大利用內存容量.
二、垃圾回收器(Garbage Collector)
垃圾回收器(Garbage Collector)是實現垃圾回收算法並應用在
JVM
環境中的內存管理模塊
(1) Serial
回收器
Serial
回收器是一個主要應用於 YGC 的垃圾回收器, 採用串行單線程的方式完成 GC 任務
“Stop The World” 簡稱 STW, 即垃圾回收的某個階段會暫停整個應用程序的執行
現在寫後臺
Java
系統幾乎不用
主要流程如圖:
(2) ParNew
回收器
ParNew
回收器主要用於新生代, 採用多線程機制
理論上4核CPU, 支持4個垃圾回收線程並行執行
執行Minor GC
, 也採用 STW, 會把系統程序的工作線程全部停掉, 禁止程序繼續運行創建新的對象, 採用多個垃圾回收線程去進行垃圾回收
-XX:+UseParNewGC
: 表示JVM
啓動之後對新生代進行垃圾回收
-XX:ParallelGCThreads
: 調節ParNew
的垃圾回收線程數量
一般採用默認, 例如: 4核CPU 8核CPU 16核CPU, 對應
ParNew
線程數爲 4、8、16
(3) CMS
回收器(Concurrent Mark Sweep Collector)
CMS
回收器 是回收停頓時間比較短、用於老年代的垃圾回收器, 採用多線程機制, 性能較好
四個步驟完成垃圾回收:
- 初始化標記(Initial Mark)
- 併發標記(Concurrent Mark)
- 重新標記(Remark)
- 併發清除(Concurrent Sweep)
1、3 步的初始化標記和重新標記階段依然會引發
STW
,
而 2、4步的併發標記和併發清除兩個階段可以和應用程序併發執行, 也是比較耗時的操作, 但並不影響應用程序的正常執行
CMS
採用 “標記 - 清除算法”, 因此產生大量的空間碎片
-XX:+UseCMSCompactAtFullCollection
強制JVM
在FGC
完成後對老年代進行壓縮, 執行一次空間碎片整理, 但是空間碎片整理階段也會引發STW
-XX:+CMSFullGCsBeforeCompaction=n
: 在執行了 n 次FGC
後,JVM
再在老年代執行空間碎片整理, 減少 STW 次數.
(4) G1
回收器(Garbage-First Garbage Collector)
目的:爲了減少Stop the World
Hotspot 在 JDK7 中推出了新一代 G1, 通過
-XX:+UseG1GC
開啓
優點:
- 和 CMS 相比, G1 具備壓縮功能, 能避免碎片問題
- G1 的暫停時間(
STW
)更加可控 - 統一收集新生代和老年代, 採用了更加優秀的算法和設計機制
- 可預測的停頓時間,能夠儘可能地在指定時間內完成空間碎片,
-XX:MaxGCPauseMills
參數來設定,默認值爲 200ms
如圖:G1
回收模型內存佈局
G1
將 Java
堆空間分割成了若干相同大小的區域,即 region
region
包括Eden
、Survivor
、Old
、Humongous
四種類型Humongous
是特殊的Old
類型,專門放置大型對象。- 這樣劃分意味着不需要一個連續的內存空間管理對象
G1
採用 Mark-Copy
G1
執行時使用 4個 worker併發執行,在初始標記時,還是會觸發STW
1. G1
是如何工作的?
G1
需要知道每個Region
有多少垃圾,處理這些垃圾需要多長時間?
根據需要回收對象的大小和回收預估時間,進行選擇回收Region
如圖:
2. 對象什麼時候進入新生代的Region
?什麼時候對象進入老年代 Region
?
剛開始,
Region
可能誰都不屬於。
- 當有對象產生,則分配給了新生代
- 觸發
GC
- 下一次,同一個
Region
可能又被分配給了老年代,用來存放老年代的對象
如圖:
新生代進入老年代的條件:
- 對象在新生代躲過了很多此的垃圾回收,達到了一定的年齡,可通過
-XX:MaxTenuringThreshold
設置年齡 - 動態年齡判定規則,如果一旦發現某次新生代 GC 過後,存貨對象超過了
Survivor
的50%
3. 什麼時候觸發Region GC
?什麼時候觸發老年代的Region GC
?
G1
將內存劃分爲多個Region
,但還是有 新生代、老年代的區分
新生代裏還是有Eden
和Survivor
劃分
觸發垃圾回收的機制也相類似
4. Humongous
大對象Region
G1
提供了專門的Region
來存放大對象,而不是讓大對象進入老年代Old
的Region
中
大對象的判定規則:
一個大對象超過一個
Region
大小的 50%,就會被放入大對象專門的Region
中
比如:
每個Region
2MB,只要一個大對象超過 1MB,就會被放入大對象專門的Region
中
如圖:
三、問題
(1) 單線程與多線程進行垃圾回收比較
單CPU運行多線程會導致頻繁的線上切換上下文, 有效率開銷
即:
- 若在客戶端(客戶端程序), 採用單線程垃圾回收器
- 若在服務端(有多核CPU資源), 採用多線程垃圾回收器
(2) ParNew
+ CMS
的 GC
, 如何保證只做 YGC
, JVM
參數如何配置?
- 加大分代年齡, 比如默認 15 加到 30
- 修改新生代和老年代的比例, 比如新生代:老年代 = 2:1
- 修改
Eden
區 和S0
S1
區比例, 例如 6: 2: 2
(3) 如何做到FullGC
次數爲0, 只做 YGC
?
關鍵點: 讓
Survivor
區能放下, 不能因爲動態年齡判斷規則直接升入老年代
觀察上線系統, 每秒會新增多少對象在新生代裏, 多長時間觸發一次 Minior GC
, 平均每次 Minor GC
之後會有多少對象存活, Survivor
區是否可以放下
(4) 有哪些參數需要了解?
2核4G機器, 可提供 JVM 最大內存 2G
- 方法區(元空間)
- 老年代
- 新生代
- 棧
(5) 爲什麼老年代的 FGC
要比 新生代Minior GC
慢很多, 一般10倍以上?
- 併發標記階段, 老年代存活對象多, 追蹤
GC Roots
花費要久 - 併發清理階段, 老年代不是一次性回收一大片內存, 而是零散
- 內存碎片整理, 把大量的存活對象給挪在一起, 空出來連續內存空間, 這個過程需要
STW
(6) 幾個觸發老年代 GC 的時機?
-
老年代可用內存小於新生代全部對象的大小, 如果沒開啓空間擔保參數, 會直接觸發
FGC
, 所有一般空間擔保參數都會打開 -
老年代可用內存小於歷次新生代
GC
後進入老年代的平均對象大小, 此時會提前FGC
-
新生代
Minior GC
後的存活對象大於Survivor
, 那麼就會進入老年代, 此時老年內存若不足 -
如果老年代可用內存大於歷次新生代
GC
後進入老年代的對象平均大小, 但是老年代已經使用的內存空間超過了這個參數指定的比例(-XX:CMSInitiatingOccupancyFaction
), 也會自動觸發FGC
(7) 多大的對象直接進入老年代?
大對象可以直接進入老年代, 因爲大對象很可能是要長期存活和使用的
一般設置超過 1MB 的對象爲大對象
(8) JVM
參數標準格式?
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGc -XX:+UseConcSweepGC