Java虛擬機器四:垃圾回收演算法與垃圾收集器
在Java執行時的幾個資料區域中,程式計數器,虛擬機器棧,本地方法棧3個區域隨著執行緒而生,隨執行緒而滅,因此這幾個區域的記憶體分配和回收具有確定性,不需要過多考慮垃圾回收問題,因為方法結束或者執行緒結束時,記憶體就回收了。但是方法區和堆區不一樣,一個介面或者實現類所需要的記憶體可能不一樣,一個方法的多個分支需要的記憶體也可能不一樣,只有程式執行時才能知道建立哪些物件,這部分記憶體的分配和回收是動態的。
在進行垃圾回收時候,首先需要判斷哪些物件需要回收,這就涉及到回收演算法的問題。
一、垃圾回收演算法
1.標記-清除演算法
標記-清除演算法是一種最基礎的垃圾收集演算法,分為“標記”和“清除”兩步。“標記”階段標記所有需要進行垃圾回收的物件,標記完成後統一回收被標記的物件。這種演算法的不足點在於:
(1)效率問題,標記和清除兩個過程效率都不高;
(2)空間問題,標記清除後會產生大量不連續碎片,後續如果需要為較大物件分配空間,則又需觸發垃圾回收。
2.複製演算法
為了解決標記-清除演算法的效率問題,出現了複製演算法。這種演算法把記憶體按照容量劃分為大小相同的兩塊,每次只是用其中一塊,當這塊記憶體用完了,就把還存活的物件複製到另外一塊中,並將這塊的記憶體清理掉,然後使用另外一塊,當另外一塊記憶體用完了,再把存活的物件複製到這塊中,並清理另外一塊記憶體,依次類推。
複製演算法主要用於新生代的回收,在HotSpot虛擬機器中,新生代記憶體劃分為一塊較大的Eden空間,和兩塊較小的Survivor空間,每次使用Eden空間和其中一塊Survivor空間。當進行垃圾回收時,會把Eden空間和Survivor空間中存活的物件一次性複製到另外一塊Survivor空間上,最後清理掉Eden空間和剛才使用過的Survivor空間。HotSpot虛擬機器中,預設情況下Eden空間和Survivor空間的大小比例是8:1,即Eden空間佔整個新生代的80%,每次新生代中使用的空間為80%+10%=90%,閒置空間10%。
3.標記-整理演算法
複製演算法適用於那種物件存活率較低的場景,在物件存活率較高時,使用複製收集演算法意味著需要進行大量複製,會使效率降低,同時複製大量存活物件到另外一塊記憶體,意味著需要有足夠大的記憶體來儲存這些物件,這勢必會降低記憶體使用率。根據老年代的特點,有人提出標記-整理演算法,和標記-清除演算法不同的是,標記整理演算法將存活的物件向一端移動,然後直接清理掉端邊界之外的記憶體。
4.分代收集演算法
目前商業虛擬機器中都使用分代收集演算法。一般將Java堆分為新生代和老年代,新生代進行垃圾收集發現有大量物件死去,只有少量物件存活,那麼就使用複製演算法。老年代中物件存活率較高,使用標記-清除演算法或者標記-整理演算法。
二、垃圾收集器
垃圾收集演算法提供了記憶體回收的方法論,垃圾收集器是記憶體回收的方法論。每個廠商對垃圾收集器的實現不一樣,這裡主要討論Jdk1.7 Update 14之後的HotSpot虛擬機器。這個虛擬機器中包含的垃圾收集器有如下7種:
以上收集器之間如果有連線,則表明可以搭配使用,虛擬機器所處區域,表示他是新生代收集器還是老年代收集器。
1.Serial收集器
Serial收集器是一種最基本的單執行緒收集器,這種收集器工作時,必須停止其他所有工作執行緒,優點在於簡單高效,但體驗很不友好,目前主要應用場合是:虛擬機器執行在Client模式下的預設新生代收集。器。
2.ParNew收集器
parNew收集器是Serial收集器的多執行緒版本,常用引數設定:
-XX:+UseConcMarkSweepGC :設定ParNew為預設的新生代收集器;
-XX:+UseParNewGC :指定使用ParNew為年輕代收集器,強制指定;
-XX:ParallelGCThreads=n :設定收集器的執行緒數為n。
3.Parallel Scavenge收集器
Parallel Scavenge收集器是一個使用複製演算法的新生代收集器,這種收集器的主要目標是達到一個可控制的吞吐量(Throughput,CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間))。由於與吞吐量關係密切,故而Parallel Scavenge收集器也稱為“吞吐量優先”收集器。常用引數設定:
-XX:MaxGCPauseMillis=n :設定年輕代垃圾收集的最長時間;
-XX:GCTimeRatio=n :設定垃圾收集總可用時長的比例,和吞吐量直接相關;
-XX:+UseAdaptiveSizePolicy 自適應大小開關,配置該選項之後,每次GC後會重新計算 Eden、From 和 To 區的大小,計算依據是 GC 過程中統計的 GC 時間、吞吐量、記憶體佔用量,因此設定此引數之後就不需要再設定 -XX:SurvivorRatio 、 -XX:PretenureSizeThreshold 等引數了。
4.Serial Old收集器
Serial收集器的老年版本,也是一個單執行緒收集器,使用的是“標記-整理”演算法,這種收集器的主要意義也是給Client模式下的虛擬機器使用。
5.Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge收集器和Parallel Old收集器的組合。
6.CMS收集器
CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。它基於“標記-清除”演算法實現,運作過程相對於其他幾種收集器更復雜一些。分為以下四個過程:
(1)初始標記(CMS initial mark):標記一下CG Roots能關聯到的物件;
(2)併發標記(CMS concurrent mark):進行CG Roots Tracing的過程;
(3)重新標記(CMS remark):修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄。
(4)併發清理(CMS concurrent sweep)
CMS 收集器的優點在於併發收集,低停頓。其缺點在於以下三點:
(1)CMS收集器對CPU很敏感,CMS預設回收執行緒是(CPU數量+3)/4,當CPU在4個以上時,併發收集時垃圾收集執行緒不少於25%的CPU資源,並隨著CPU數量增加而下降。但是當CPU不足4個時,CMS對使用者程式的影響就會變得很大。
(2)CMS收集器無法處理浮動垃圾。由於CMS收集器併發清理階段使用者執行緒還在執行著,伴隨著程式執行就會有垃圾產生,這部分垃圾在標記過後,CMS收集器無法在當次收集中清理這些垃圾。
(3)由於CMS收集器是一種基於“標記-清除”演算法的收集器,這種演算法實現的收集器在收集結束後會有大量不連續碎片產生。碎片過多時會給大物件分配帶來很大麻煩,往往老年代還有很大空間剩餘,但是無法找到連續空間分配當前物件,因而不得不提前觸發Full GC。
7.G1收集器
G1收集器是一款面向服務端應用的垃圾收集器,與其他收集器相比,G1收集器具有如下優點:
(1)併發與並行:G1能充分利用多CPU,多核硬體優勢,使用多個CPU來減少停頓時間;
(2)分代收集:G1不需要其他收集器配合就能獨立管理整個堆的垃圾收集,且它能採用不同方式去處理新建物件和已經存活了一段時間,熬過多次GC的舊物件以獲得更好的收集效果。
(3)空間整合:使用G1收集器不會產生記憶體碎片,收集後能提供規整的可用記憶體。這種特性有利於程式長時間執行,分配大物件時候不會因為無法找到連續記憶體空間而提前觸發下一次GC.
(4)可預測的停頓:G1除了追求低停頓,還能建立可預測的停頓時間模型,能讓使用著指定在長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不超過N毫秒。
三、垃圾收集引數總結
引數 | 描述 |
---|---|
UseSerialGC | 虛擬機器執行在Client模式下的預設值,開啟此開關後,使用Serial+Serial Old的收集器組合進行記憶體回收 |
UseParNewGC | 開啟此開關後,使用ParNew + Serial Old 的收集器組合進行記憶體回收 |
UseConcMarkSweepGC | 開啟此開關後,使用ParNew + CMS + Serial Old 的收集器組合進行記憶體回收。Serial Old 收集器將作為CMS收集器出現Concurrent Mode Failure失敗後的後備收集器使用 |
UseParallelGC | 虛擬機器執行在Server 模式下的預設值,開啟此開關後,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進行記憶體回收 |
UseParallelOldGC | 開啟此開關後,使用Parallel Scavenge + Parallel Old 的收集器組合進行記憶體回收 |
SurvivorRatio | 新生代中Eden 區域與Survivor 區域的容量比值,預設為8,代表Eden :Survivor=8∶1 |
PretenureSizeThreshold | 直接晉升到老年代的物件大小,設定這個引數後,大於這個引數的物件將直接在老年代分配 |
MaxTenuringThreshold | 晉升到老年代的物件年齡。每個物件在堅持過一次Minor GC 之後,年齡就加1,當超過這個引數值時就進入老年代 |
UseAdaptiveSizePolicy | 動態調整Java 堆中各個區域的大小以及進入老年代的年齡 |
HandlePromotionFailure | 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden 和Survivor 區的所有物件都存活的極端情況 |
ParallelGCThreads | 設定並行GC 時進行記憶體回收的執行緒數 |
GCTimeRatio | GC 時間佔總時間的比率,預設值為99,即允許1% 的GC 時間。僅在使用Parallel Scavenge 收集器時生效 |
MaxGCPauseMillis | 設定GC 的最大停頓時間。僅在使用Parallel Scavenge 收集器時生效 |
CMSInitiatingOccupancyFraction | 設定CMS 收集器在老年代空間被使用多少後觸發垃圾收集。預設值為68%,僅在使用CMS 收集器時生效 |
UseCMSCompactAtFullCollection | 設定CMS 收集器在完成垃圾收集後是否要進行一次記憶體碎片整理。僅在使用CMS 收集器時生效 |
CMSFullGCsBeforeCompaction | 設定CMS 收集器在進行若干次垃圾收集後再啟動一次記憶體碎片整理,僅在使用CMS 收集器時生效 |
參考資料:《深入理解Java虛擬機器 JVM高階特性與最佳實踐 第2版》