垃圾回收器總結(一)

胖毛發表於2020-06-30

上一篇我們介紹瞭如果要使用自動記憶體管理以及垃圾回收,應該如何做,有哪些難點以及解決方法,接下可以說說在HotSpot虛擬機器中,使用過的經典的垃圾回收器:

單執行緒垃圾回收器

Serial / Serial Old 收集器

Serial收集器是最初的垃圾回收器,與其配套的是Serial Old垃圾回收器,其執行方式也和其名字一樣,是一個單執行緒垃圾回收器,新生代使用了標記-複製演算法,老年代使用標記-整理演算法。整個回收過程,都需要STW

ParNew / CMS 收集器

ParNewSerial的多執行緒版本,其回收過程和Serial非常型別,不過回收過程使用多執行緒就行回收。不過並沒有與之配套的ParOld垃圾回收器,而是CMSCMS作為一款優秀的併發垃圾回收器,預設使用ParNew與之配合。不過在JDK9之後,HotSpot官方刪除了ParNewCMS與其他垃圾回收器的搭配,比如ParNew+Serial OldSerial+CMS (一般沒有人這樣使用)。對於CMS垃圾回收器,後續會詳細介紹

ParNew使用的標記-複製演算法,CMS使用的標記-清除演算法。

Parallel Scavenge / Parallel Old 收集器

Parallel Scavenge / Parallel Old 是一款注重吞吐量的垃圾回收器,Parallel Svavenge用於回收新生代,使用標記-複製演算法,Parallel Old用於回收老年代,使用標記-整理演算法,PSJDK8的預設垃圾收集器

Paralleel Scavenge系列和CMS的區別在於注重點不同:

  • PS主要以吞吐量為重,如果執行的是那種大型的科學計算,或者是後臺日誌分析,則可以使用PS垃圾回收器。
  • CMS則是以減少停頓時間為主,對於網際網路網站,或者作為使用者APP伺服器這種直接與使用者互動的時候,可以使用CMS垃圾回收器。

G1 收集器

G1收集器是一款包乾了新生代和老年代回收的收集器,因為其新生代和老年代的大小,數量,位置都是動態變化的,G1通過建立優先順序列表,每次都進行部分垃圾回收,實現了 “可預測停頓時間模型”。

G1收集器,在總體上看,是基於標記-整理演算法,但是從區域性(兩個Region)來看,是基於標記-複製演算法。


CMS

下面詳細介紹下CMSG1的回收過程,CMS雖然後續會被逐漸標記為Deprecated(JDK14已經無法使用),但是目前作為小型應用伺服器還是有比較好的效果,並且其回收過程是直接借鑑的。

CMS(ConcurrentMarkSweep)

-XX:+UseConcMarkSweepGC表示老年代使用CMS回收器,新生代預設使用ParNewCMS是一款以減少停頓時間為主的垃圾回收器,從名字就能看出來:併發,標記清除回收器。CMS的回收過程如下:

  • 初始標記(STW initial mark)
  • 併發標記(Concurrent marking)
  • 併發預清理(Concurrent precleaning)
  • 重新標記(STW remark)
  • 併發清理(Concurrent sweeping)
  • 併發重置(Concurrent reset)

初始標記: 初始標記便是標記GC Roots的過程, 這個過程需要Stop The Word,不過時間比較短。

併發標記: 併發標記是從初始標記標記出來的物件出發,開始迴圈查詢(Trace)的過程。這個過程比較長,不過它可以不用暫停使用者執行緒,可以和使用者執行緒併發。併發帶來的好處是使用者感受不到停頓,但是會佔用CPU和產生併發失敗的問題,佔用CPU比較好理解。因為CMS需要和使用者執行緒併發。併發失敗,是因為由於使用者執行緒此時正在執行,如果這個時候JVM預留的記憶體不能滿足使用者執行緒申請的記憶體,就會發生併發失敗,併發失敗的後果就是CMS立即暫停使用者執行緒,然後使用單執行緒開始回收記憶體(類似Serial GC

併發預清理 : 併發預清理依然是併發進行的,這一步主要是為了減輕下一階段:重新標記的工作,減少重新標記的停頓時間。主要做的工作有:處理跨代引用,處理併發標記過程老年代引用關係變化的物件(三色標記,增量更新)

可中斷預清理: 這個階段的目標跟“預清理”階段相同,也是為了減輕重新標記階段的工作量。為什麼叫可中斷預清理,原因在於"記憶集",CMS為了節省新生代記憶集(RSET)的開銷(因為新生代朝生夕死,變動很大,維護起來開銷比較大),所以只實現了老年代指向新生代的RSET。這樣做的代價便是每次進行Old GC的時候,就需要掃描整個新生代。如果新生代物件數量過多,則會導致remark階段暫停時間過長,所以可中斷預處理的主要目的就是為了等待一次minor gc,減少新生代物件數量。

和這個階段有關的控制引數:

-XX:CMSScheduleRemarkEdenSizeThreshold: 預設2M,新生代使用大小低於這個值的話,不啟動可中斷預清理

-XX:CMSScheduleRemarkEdenPenetration:新生代使用率,預設50%,大於這個值就結束可中斷預清理

可中斷預處理會一直迴圈,直到時間超過CMSMaxAbortablePrecleanTime(預設5s),或者次數超過CMSMaxAbortablePrecleanLoops(預設為0),表示只以時間為準

可中斷預處理只是用來等待minor gc,在某些情況下,如果沒有等到minor gc,則可以通過設定CMSScavengeBeforeRemark表示每次進入remark階段之前,都進行一次minor gc

重新標記: 這個階段主要就是為了修正前面併發標記過程中,使用者執行緒修改的引用關係。主要包括通過增量更新記錄的併發過程中新插入的物件以及掃描整個新生代,查詢被新生代引用的老年代物件。這個過程作為最後的糾正過程,需要Stop The Word,同時由於CMS沒有實現新生代對老年代引用的RSet,因此這個過程需要掃描整個新生代。

前面說過CMS由於新生代朝生夕死,變化較大,所以為了節省開銷沒有實現新生代指向老年代的RSet,那麼其他垃圾回收期為什麼沒有這個問題呢?原因在於只有CMS存在單獨的Old GC,也就是隻針對老年代的GC,其他GC大多數Old GC都是Full GC,也就是回收整個堆,因此便不存在這個問題,而對於G1這種Mixed GC的,是每個Region都單獨存在一個RSet的。

併發清除CMS採用標記-清除演算法回收老年代記憶體,這樣帶來的好處便是不用移動物件,效率高,並且方便實現併發回收,但是缺點也很明顯,那就是存在記憶體碎片, 同時分配記憶體無法通過指標碰撞的方式進行計算,只能通過"分割槽空閒分配表"查詢可分配的記憶體,會對分配記憶體帶來一定的效率問題。

併發重置: CMS恢復內部初始值,為下一次記憶體回收做準備。


從GC日誌上看CMS回收過程

CMS的回收過程大體上分析完畢,但是想要真正理解,還需要具體的實踐。下面從CMS的日誌開始對上面說到的回收過程進行分析。

/**
  初始標記:
  老年代已使用410547K(總共449900K)]
  當前堆已使用414147K,總容量652396K,
  標記使用時間:0.0004938 secs 
  */
[GC (CMS Initial Mark) [1 CMS-initial-mark: 410547K(449900K)] 414147K(652396K), 0.0004938 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

/**
  併發標記開始
*/
[CMS-concurrent-mark-start]
/**
  併發標記,已使用時間:0.001 secs
*/
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

/**
  併發預清理開始
*/
[CMS-concurrent-preclean-start]
/**
  併發預清理使用時間
*/
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
/**
  可中斷併發預清理開始
*/
[CMS-concurrent-abortable-preclean-start]
/**
  Minor GC ,ParNew,GC原因:記憶體分配失敗    回收前3600K,回收後:4K,回收使用時間:0.000655 secs
*/
[GC (Allocation Failure) [ParNew: 3600K->4K(202496K), 0.0006552 secs][CMS[CMS-concurrent-abortable-preclean: 0.000/0.024 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
 
/** 
  併發回收失敗,回收前老年代使用410547K->回收後使用947K(總容量449900K)
  總容量已使用414147K->回收後使用947K,總共652396K
  元空間:回收前3729K->回收後3729K,總容量1056768K
  使用時間:0.0195286 secs
*/
 (concurrent mode failure): 410547K->947K(449900K), 0.0050302 secs] 414147K->947K(652396K), [Metaspace: 3729K->3729K(1056768K)], 0.0195286 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]  
/**
 由於併發失敗,CMS剛剛已經被替換為Serial Old 進行Full GC,因此後面沒有繼續remark ,而是重新開始新的一輪迴收。
*/
[GC (CMS Initial Mark) [1 CMS-initial-mark: 615347K(789192K)] 615347K(867912K), 0.0005200 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]
//省略重複的日誌,我們接著可中斷併發預清理後面,正常情況便是remark
/**
  重新標記:年輕代已使用1400K,容量為78720K
  多執行緒重新掃描,使用時間0.0005394 secs
  清理弱引用:weak refs processing
  解除安裝未使用的類:class unloading
  分別清理包含類級後設資料和內部化字串的符號表和字串表:scrub symbol table,scrub string table
  這個階段老年代使用量和老年代容量:615347K(789192K)
  這個階段總的使用率和總的容量:616747K(867912K)
  使用時間: 0.0011634 secs
*/
[GC (CMS Final Remark) [YG occupancy: 1400 K (78720 K)][Rescan (parallel) , 0.0005394 secs][weak refs processing, 0.0000043 secs][class unloading, 0.0001943 secs][scrub symbol table, 0.0003117 secs][scrub string table, 0.0000867 secs][1 CMS-remark: 615347K(789192K)] 616747K(867912K), 0.0011634 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
/**
 併發清理開始
*/
[CMS-concurrent-sweep-start]
/**
 併發清理
*/
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
/**
 併發重置開始
*/
[CMS-concurrent-reset-start]
/**
 併發重置
*/
[CMS-concurrent-reset: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

從日誌上我們可以看出CMS的整個階段和我們剛開始整理的差不多,也能看出來CMSFinal Remark階段任務是比較繁重的。

上面的日誌中出現了"併發失敗",這個現象其實非常容易復現,JDK 8預設的預留容量是92%,也就是說你要是不斷地while迴圈申請大於8%的物件,同時設定-XX:PreteureSizeThreshold(大物件直接進入老年代引數)小於8%容量,基本就能復現併發失敗的現象。

CMS 調優

明白了上面CMS的回收過程,我們就能簡單的進行調優:

  • GC 日誌中出現concurrent mode failure:前面我們已經解釋了併發失敗出現的原因,出現併發失敗後,會啟動Serial Old重新進行回收,會浪費很多的時間,因此我們需要避免這個問題,避免的方式也比較簡單,減少-XX:CMSInitiatingOccupancyFraction的值,預設92%
  • GC日誌中Remark時間過長: remark階段是一個需要Stop The Word的過程,同時這個過程需要掃描整個新生代,如果發現現場日誌中remark階段相對整個過程比較長,同時可中斷預清理沒有等到Yong GC,那麼可以設定:-XX:CMSScavengeBeforeRemark,強制在remark階段之前進行Yong GC
  • GC日誌中發現Full GC過於頻繁,CMS作為唯一個可以只針對老年代回收的垃圾回收器,如果Full GC過於頻繁,那說明出現了很多次CMS無法處理的情況,情況之一便是記憶體碎片,CMS

以上只是CMS簡單的調優,還有一些例如記憶體碎片整理等等,由於這些引數在JDK9中已經被廢棄,同時作用不是很大,感興趣的同學可以後面瞭解。


說完了CMS後面將會對比介紹G1,同時將會簡單的介紹JDK自帶的一些除錯工具。

關注我,帶你瞭解不一樣的讀書筆記

相關文章