深入JVM-垃圾收集算法與垃圾收集器

1.垃圾收集算法

1.1.標記-清除算法

此算法分爲“標記”和“清除”兩個階段,首先標記出所有要回收的對象,標記完成後統一回收掉所有被標記的對象(標記過程參考《深入JVM-垃圾收集器之內存回收》),它是最基礎的收集算法,其缺點主要有兩個:

  1. 效率問題,標記和清除過程效率不高
  2. 空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致,程序在以後運行過程中需要分配大對象時找不到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
    標記-清除算法執行過程如下:
    在這裏插入圖片描述

1.2.複製算法

此算法相對於“標記-清除算法”解決了效率問題,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了,就將還存活的對象複製到另外一塊上,然後把已使用過的內存塊一次清理掉。這樣每次都是對其中一塊進行內存回收,內存分配時就不用考慮內存碎片等複雜情況,只要一動對頂指針按順序分配內存即可,實現簡單,運行高效。
此算法缺點是將內存每次只能使用一半,代價太高。
複製算法執行過程如下:
在這裏插入圖片描述

1.3.標記-整理算法

標記-整理算法的標記過程和“標記-清除算法”一樣,但是後續步驟不是直接回收對象,而是讓所有存活的對象都想一端移動,然後直接清理掉端邊界以外的內存。
標記-整理算法執行過程如下:
在這裏插入圖片描述

1.4.總結

當前商業虛擬機的垃圾收集都是使用“分代收集”算法,其實就是根據對象的存活週期不同將內存劃分爲爲幾塊。一般java堆分爲新生代和老年代。

  • 新生代
            新生代中的對象生命週期短並且對象的存活率低,故一般都是使用複製算法來回收新生代,新生代中內存分爲一個Eden空間和兩個Survivor空間,Eden空間和Survivor比例爲8:1:1,每次只使用Eden空間和一個Survivor空間,當回收時,將Eden空間和Survivor空間中還存活的對象一次性拷貝到另外一個Survivor空間中,如果這個Survivor沒有足夠的內存空間,則會通過分配擔保進入到老年代,然後清理掉Eden和剛纔使用過的Survivor空間。故新生代中可用內存容量爲整個新生代內存的90%(80%+10%)。
  • 老年代
            老年代中對象存活率相對較高,也沒有額外空間對它進行分配擔保,因此使用標記-整理算法來進行內存回收。

2.垃圾收集器

並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。
併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序繼續執行,而垃圾收集程序運行與另一個CPU上。

2.1.Serial收集器

Serial收集器是一個單線程的收集器,工作只能使用一個CPU或者一個收集線程去完成垃圾收集,還有就是在進行垃圾收集時,必須暫停其他所有的工作線程(Stop The World),直到收集結束。
特點

  • 單線程環境下,與其他收集器相比簡單而高效。
  • 沒有線程間的交互,一般作爲虛擬機在Client模式下的默認的新生代收集器。

2.2.ParNew收集器

ParNew收集器其實是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行爲(JVM控制參數、收集算法、Stop The World、對象分配規則、回收策略)都與Serial收集器完全一樣。
特點

  • 新生代中一般選擇ParNew收集器或者Serial收集器中的一個。
  • 單線程環境下效率不如Serial收集器。
  • 多線程環境下對於GC時系統資源利用較好,默認開啓的線程數與CPU數量相同。

2.3.Parallel Scavenge收集器

Parallel Scavenge收集器也是新生代收集器,也是使用複製算法,而且是並行多線程收集器。它的關注點與其他收集器不同,其他收集器關注點是儘可能的縮短垃圾收集時用戶線程停頓時間,而Parallel Scavenge收集器的目的是達到一個可控制的吞吐量。吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。
        停頓時間越短越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗;而高吞吐量則可以最高效率的利用CPU時間,儘快地完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。
Parallel Scavenge收集器提供兩個參數控制吞吐量:

  • -XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間
            MaxGCPauseMillis是一個大於0的毫秒數,收集器將盡力保證內存回收花費的時間不超過設定值。但是不能認爲這個參數越小就會使系統的垃圾收集越快,因爲GC停頓時間是以犧牲吞吐量和新生代空間換取的。例如把系統新生代調小點,收集300MB肯定比收集500MB快,但是也導致收集更頻繁,原來10秒收集一次、每次停頓100毫秒,現在變成每5秒收集一次、每次停頓70毫秒。停頓時間是降低了,但是吞吐量也降下來了。
  • -XX:GCTimeRatio 直接設置吞吐量大小
            GCTimeRatio是一個大於0小於100的整數。如果此數爲n,那麼系統所允許的最大GC時間就是1/(1+n)。

Parallel Scavenge收集器GC自適應調節策略
        此收集器有個參數-XX:UseAdaptiveSizePolicy,這個參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升到老年代的年齡(-XX:PretenureSizeThreshold)等細節參數,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量。
        如果對收集器原理不太瞭解,或者手工優化存在困難時候,使用此策略是個很不錯的選擇,只需要把內存數據設置好,然後使用MaxGCPauseMillis參數或GCTimeRatio參數給虛擬機設立一個優化目標,具體細節參數的調節工作就由虛擬機完成了。

2.4.Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它也是一個單線程收集器,使用“標記-整理”算法,主要也是被Client模式下的虛擬機使用。但是在Server模式下也有兩個用途:
一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用;另一個就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure的時候使用。

2.5.Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是JDK1.6出現,在此之前,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器別無選擇,但是由於Serial Old收集器是單線程無法充分利用多服務器多CPU的處理能力,即使使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果。直到Parallel Old收集器出現後,使用Parallel Scavenge收集器搭配Parallel Old收集器,在注重吞吐量及CPU資源敏感的場合,是一種比較好的選擇。

2.6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。常用於比較重視服務的響應速度,希望系統停頓時間短的B/S服務器上,以給用戶帶來較好的體驗。
CMS收集器基於“標記-清除”算法,運行過程分爲四個步驟:

  1. 初始標記(CMS inital mark)
  2. 併發標記(CMS concurrent mark)
  3. 重新標記(CMS remark)
  4. 併發清除(CMS concurrent sweep)

其中初始標記、重新標記着兩個步驟仍然需要“Stop The World”,初始標記只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是記性GC Roots Tracing的過程,而重新標記階段則是爲了修正併發標記期間,因用戶程序繼續運行而導標記產生變動的那一部分的對象的標記記錄,這個階段的停頓時間一般會比初始標記階段長一點,但遠比並發標記時間短。

        由於整個過程中耗時最長的併發標記併發清除過程中收集器線程都是可以與用戶線程一起工作,所以總體來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。
在這裏插入圖片描述
CMS收集器還有三個缺點:

  • CMS收集器堆CPU資源非常敏感
            面向併發設計的程序都對CPU資源比較敏感,在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程(或CPU資源)而導致應用程序變慢,總吞吐量降低。CMS默認啓動的回收線程數是(CPU數量+3)/4,當CPU大於4個時,併發回收時垃圾收集線程最多佔用不超過25%的CPU資源,CPU小於4個時,那麼CMS對用戶程序的影響就可能變的很大。
  • CMS收集器無法處理浮動垃圾(Floating Garbage)
            CMS併發清理階段用戶線程還在運行着,伴隨着程序的運行還會有新的垃圾產生,由於這部分垃圾出現在標記過程後,CMS無法在本次收集中處理掉它們,只好留待下一次GC時將其清理掉,這部分垃圾被稱爲“浮動垃圾”。
            由於垃圾收集階段用戶線程還需要運行,即還需要留出足夠內存給用戶線程使用,因此CMS收集器不能像其他收集器一樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程序運作使用,默認設置爲老年代使用68%後悔激活CMS收集。
  • CMS收集器是基於“標記-清除”算法,收集結束會產生大量空間碎片
            CMS是基於“標記-清除”算法實現,即在收集結束後會產生大量空間碎片,空間碎片過多時,會造成大對象沒有足夠的內存分配,而不得不提前觸發一次Full GC。而CMS提供了一個參數-XX:UseCMSCompactAtFullCollection在Full GC之後提供一次碎片整理過程,而內存整理過程是無法併發的,即停頓時間又會變長。而虛擬機還有另一個參數-XX:CMSFullGCsBeforeCompaction用來設置在執行多少次不壓縮的Full GC後,再執行一次壓縮的操作。

2.7.G1收集器

G1收集器相對於CMS收集器有兩個改進:

  • 一是基於“標記-整理”算法實現的 收集器,即它不會產生空間碎片。
  • 二是它可以非常精準的控制停頓,即它能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

G1收集器可以實現在基本不犧牲吞吐量的前提下完成低停頓的內存回收,因爲它能夠激勵的避免全區域的垃圾收集,之前的收集器收集的範圍都是整個新生代或者老年代,而G1收集器將整個Java堆(新生代和老年代)劃分爲了多個大小固定的獨立區域Region,並且跟蹤這個區域裏面的垃圾堆積程度,在後臺維護一個有限列表,每次根據允許的收集時間,優先回收垃圾最多的區域。區域劃分及有優先級的區域回收,保證了G1收集器在優先時間內可以獲得最高的手機效率。

2.8.垃圾收集器參數總結

參數 描述
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虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章