上一節:《JVM之GC算法》 知道GC算法的理論基礎,我們來看看具體的實現。只有落地的理論,才是真理。
一、JVM垃圾回收器的結構
JVM虛擬機規范對垃圾收集器應該如何實現沒有規定,因為沒有最好的垃圾收集器,只有最適合的場景。
圖中展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,則說明它們可以搭配使用。虛擬機所處的區域則表示它是屬於新生代還是老年代收集器。
7種:serial收集器、parnew收集器、parallel scavenge收集器、serial old 收集器、parallel old收集器、cms收集器、g1收集器(整堆收集器)、
串行收集:單垃圾收集線程,進行收集工作,用戶進程需要等待
並行收集:工作原理與串行一樣,只是在收集垃圾時是多條線程同時進行,收集的效率在一般情況下自然高於單線程。
並發收集:指用戶線程與垃圾收集線程同時工作(並發:同一時間間隔)。用戶程序在繼續運行,而垃圾收集程序運行在另一個CPU上。
吞吐量:吞吐量就是CPU中用於運行用戶代碼的時間與CPU總消耗時間的比值(吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間))
1、Serial收集器
Serial(串行)收集器:最基本,最古老的收集器,只有一個線程進行垃圾收集器的工作,並且在進行垃圾收集工作時需要暫停其他工作線程(stop the word),直到他工作結束;
Serial收集器簡單高效,工作時沒有線程交互的開銷,所以可以獲得很高的單線程收集效率,對於運行在Client模式下的虛擬機來說很適合。
"-XX:+UseSerialGC":添加該參數來顯式的使用Serial垃圾收集器。
2、Serial Old收集器
Serial Old收集器是Seria收集器的老年代版本,他同樣是一個單線程收集器,使用" 標記-整理" 算法。
Serial Old收集器主要用於Client模式下的虛擬機使用。
Server模式下的兩大用途:
- 在JDK1.5及之前的版本與Parallel Scavenge收集器搭配使用;
- 作為CMS收集器的后備方案,在並發收集發生Conturrent Mode Failure時使用。
3、ParNew 收集器
ParNew(並行)收集器就是Serial收集器的多線程版本,除了在收集垃圾時是啟用多線程並行執行,其他行為(控制參數、收集算法、回收策略/Stop The Word、對象分配規則)完全一樣
應用場景:ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為它是除了Serial收集器外,唯一一個能與CMS收集器配合工作的。
"-XX:+UseConcMarkSweepGC":指定使用CMS后,會默認使用ParNew作為新生代收集器。
"-XX:+UseParNewGC":強制指定使用ParNew。
"-XX:ParallelGCThreads":指定垃圾收集的線程數量,ParNew默認開啟的收集線程與CPU的數量相同。
4、Parallel Scavenge收集器
Parallel Scavenge收集器 類似於 ParNew 收集器, Parallel Scavenge收集器 更加關注吞吐量(高效的CPU利用率)。CMS等垃圾收集器關注更多的是用戶線程的停頓時間(提搞用戶體驗);Parallel Scavenge 收集器提供很多參數供我們找到最合適的停頓時間或者最大吞吐量。JDK1.8 默認的方式;
Parallel Scavenge收集器提供了兩個參數來用於精確控制吞吐量,一是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis參數,二是控制吞吐量大小的 -XX:GCTimeRatio參數;
- “ -XX:MaxGCPauseMillis” 參數允許的值是一個大於0的毫秒數,收集器將盡可能的保證內存垃圾回收花費的時間不超過設定的值(但是,並不是越小越好,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,如果設置的值太小,將會導致頻繁GC,這樣雖然GC停頓時間下來了,但是吞吐量也下來了)。
- “ -XX:GCTimeRatio”參數的值是一個大於0且小於100的整數,也就是垃圾收集時間占總時間的比率,默認值是99,就是允許最大1%(即1/(1+99))的垃圾收集時間。
- “-XX:UseAdaptiveSizePolicy”參數是一個開發,如果這個參數打開之后,虛擬機會根據當前系統運行情況收集監控信息,動態調整新生代的比例、老年大大小等細節參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱為GC自適應的調節策略。
應用場景:注重高吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old 收集器。
5、Paraller Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。
在JDK1.6中才出現。
6、CMS(Conturrent Mark Sweep)收集器
CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS收集器是基於“標記-清除”算法實現,它的整個運行過程可以分為:
- 初始標記:標記一下GC Roots能直接關聯到的對象,這個過程速度很快,但是會暫停其他用戶線程(Stop the word)
- 並發標記:進行GCRoots Tracing的過程,同時開啟GC和用戶線程,用一個閉包的結構去記錄可達對象,但是在這個階段結束,該閉包不能保證其包含當前所有的可達對象。因為用戶進程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法會跟蹤記錄這些發生引用更新的地方。
- 重新標記:修正並發標記期間因用戶線程繼續運作而導致標記產生變動的那一部分對象的標記記錄,該階段會GC停頓,停頓時間比初始標記時間稍長,單遠比並發標記時間短。
- 並發清除:開啟用戶線程,同事GC線程清除死亡的對象
CMS收集器運行的整個過程中,最耗費時間的是並發標記和並發清除,GC收集器線程和用戶線程是一起工作的,所以總體來說,CMS收集器的內存回收過程是與用戶線程一起並發執行的。
優點:並發收集、低停頓。
缺點:
- 1、CMS收集器對CPU資源非常敏感。雖然在兩個並發階段不會導致用戶線程停頓,但是會因為占用了一部分線程而導致應用程序變慢,總吞吐量下降。CMS默認啟動的回收線程數是(CPU數量+3)/4。
- 2、:CMS收集器無法處理浮動垃圾,可能出現“Conturrent Mode Failure”失敗而導致另一次Full GC產生。由於CMS並發清除階段用戶線程還在運行,伴隨着程序還在產生新的垃圾,這一部分垃圾出現在標記之后,CMS無法在當次收集中處理掉它們,只能留到下次再清理,這一部分垃圾稱為“浮動垃圾”。也正是由於在垃圾收集階段用戶線程還在運行,那么也就需要預留有足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等待老年代填滿之后再進行收集,需要預留一部分空間給並發收集時用戶程序使用。可以通過“-XX:CMSInitiatingOccupancyFraction”參數設置老年代內存使用達到多少時啟動收集。
- 3、:由於CMS收集器是一個基於“標記-清除”算法的收集器,那么意味着收集結束會產生大量碎片,有時候往往還有很多內存未使用,但是沒有一塊連續的空間來分配這個大對象,導致不得不提前觸發一次Full GC。CMS收集器提供了一個“-XX:UseCMSCompactAtFullCollection”參數(默認是開啟的)用於在CMS收集器頂不住要FullGC時開啟內存碎片整理(內存碎片整理意味着無法並發執行不得不停頓用戶線程)。參數“-XX:CMSFullGCsBeforeCompaction”來設置執行多少次不壓縮的Full GC后,跟着來一次帶壓縮的(默認值是0,意味着每次進入Full GC時都進行碎片整理)。
-server -Xmx2048m -Xms2048m -Xmn706m -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
-XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly
-XX:MetaspaceSize=256M -Xss256k -XX:CMSInitiatingOccupancyFraction=70 -XX:SurvivorRatio=8
7、G1(Garbage-First)收集器
G1的內存模型
G1收集器沒有新生代和老年代的概念,而是將Java堆划分為一塊塊獨立的大小相等的Region。當要進行垃圾收集時,首先估計每個Region中的垃圾數量,每次都從垃圾回收價值最大的Region開始回收,因此可以獲得最大的回收效率
Humongous是特殊的Old類型,專門放置大型對象.這樣的划分方式意味着不需要一個連續的內存空間管理對象.G1將空間分為多個區域,優先回收垃圾最多的區域.
G1采用的是Mark-Copy ,有非常好的空間整合能力,不會產生大量的空間碎片
G1的一大優勢在於可預測的停頓時間,能夠盡可能快地在指定時間內完成垃圾回收任務,在JDK11中,已經將G1設為默認垃圾回收器,通過jstat命令可以查看垃圾回收情況,在YGC時S0/S1並不會交換.
一個對象和它內部所引用的對象可能不在同一個Region中,那么當垃圾回收時,是否需要掃描整個堆內存才能完整地進行一次可達性分析?
當然不是,每個Region都有一個Remembered Set,用於記錄本區域中所有對象引用的對象所在的區域,從而在進行可達性分析時,只要在GC Roots中再加上Remembered Set即可防止對所有堆內存的遍歷.
回收步驟
初始標記:標記與GC Roots直接關聯的對象,停止所有用戶線程,只啟動一條初始標記線程,這個過程很快.
並發標記:進行全面的可達性分析,開啟一條並發標記線程與用戶線程並行執行.這個過程比較長.
最終標記:標記出並發標記過程中用戶線程新產生的垃圾.停止所有用戶線程,並使用多條最終標記線程並行執行.
篩選回收:回收廢棄的對象.此時也需要停止一切用戶線程,並使用多條篩選回收線程並行執行.
G1為什么能建立可預測的停頓時間模型?
因為它有計划的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的大小,在后台維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。這樣就保證了在有限的時間內可以獲取盡可能高的收集效率。
G1與其他收集器的區別?
其他收集器的工作范圍是整個新生代或者老年代、G1收集器的工作范圍是整個Java堆。在使用G1收集器時,它將整個Java堆划分為多個大小相等的獨立區域(Region)。雖然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔離的,他們都是一部分Region(不需要連續)的集合。
二、如何選擇垃圾收集器
1、單CPU或者小內存,單機程序 — -XX:+UseSerialGC
2、多CPU,需要大吞吐量,如后台計算型應用,允許工作線程停頓超過1秒 -XX:+UseParallelGC + -XX:+UseParallelOldGC
3、多CPU,追求低停頓時間,快速響應如互聯網應用 -XX:+UseParNewGC + -XX:+UseConcMarkSweepGC
4、JVM自己選擇
5、官方推薦G1,高性能