Java虛擬機學習筆記(三):垃圾收集器

垃圾收集器

我們討論的收集器基於JDK1.7 Update14之後的HotSpot虛擬機,這個虛擬機包含的所有收集器如圖:

HotSpot虛擬機的垃圾收集器

連線說明可以搭配使用

Serial收集器

Serial是最基本、發展歷史最悠久的收集器,在JDK1.3之前是虛擬機新生代收集的唯一選擇。Serial收集器是一個單線程的收集器,單線程的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束

以下是Serial/Serial Old收集器的運行過程:

Serial收集器運行過程

Serial收集器是虛擬機運行在Client模式下的默認新生代收集器。它有着優於其它收集器的地方:簡單而高效(與其它收集器的單線程相比),對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率

ParNew收集器

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

以下是ParNew收集器的運行過程:

ParNew收集器運行過程

ParNew收集器除了多線程收集之外,其他的與Serial收集器相比並沒有太多創新之處,但它卻是許多運行在Server模式下的虛擬機中新生代收集器,其中有一個與性能無關但很重要的原因是:除了Serial收集器之外,目前只有它能與CMS收集器配合工作。

CMS是HotSpot虛擬機中第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。在JDK1.5推出

從ParNew收集器開始,後面還會接觸到幾款併發和並行的收集器,這裏我們先來介紹一下並行和併發的概念:

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

Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。其特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量

吞吐量是指CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小-XX:GCTimeRatio參數。

MaxGCPauseMillis參數允許的值是一個大於0的毫秒數,收集器將儘可能地保證內存回收花費的時間不超過設定值。不過需要注意的是GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代肯定比收集500MB快,但是這也直接導致垃圾收集發生得更加頻繁。簡而言之,停頓時間的確在下降,但吞吐量也在下降

GCTimeRatio參數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數

由於與吞吐量關係密切,Parallel Scavenge收集器也經常稱爲“吞吐量優先”收集器。除了上述兩個參數之外,Parallel Scavenge收集器還有一個參數-XX:UseAdaptiveSizePolicy值得關注。這是一個開關參數,當這個參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱爲GC自適應的調節策略。

自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。如果在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

以下是Serial Old收集器的運行過程:

Serial收集器運行過程

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器

以下是Parallel Old的運行流程:

Parallel Old收集器

JDK1.6開始提供

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它是基於“標記-清除”算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分爲4個步驟:

  • 初始標記
  • 併發標記
  • 重新標記
  • 併發清除

其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。

初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快

併發標記階段就是進行GC Roots Tracing的過程

重新標記階段則是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

由於整個過程中耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,所以從總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。

以下是CMS收集器的執行流程:

CMS收集器

優點:

  • 併發收集
  • 低停頓

缺點:

  1. CMS收集器對CPU資源非常敏感

    在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS默認啓動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程會佔用不少於25%的資源,並且隨着CPU數量的增加而下降。

    那麼問題來了,當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大,如果本來CPU負載就比較大,還分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低50%。爲了應付這種情況,虛擬機提供了一種稱爲“增量式併發收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器變種,它會在併發標記、清理的時候讓GC線程、用戶線程交替運行,儘量減少GC線程獨佔資源的時間,這樣整個垃圾收集的過程會更長,但對用戶程序的影響就會顯得少一些,也就是速度下降沒有那麼明顯

  2. CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生

    浮動垃圾:在併發清理過程中產生的垃圾要下次GC纔回收

    在JDK1.5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活,這是一個偏保守的設置,如果在應用中老年代增長不是太快,可以適當調用參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以便降低內存回收次數從而獲取更好的性能,在JDK1.6中,CMS收集器的啓動閾值已經提升至92%。

    要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長

  3. CMS是一款基於“標記-清除”算法實現的收集器,所以收集結束時會有大量空間碎片產生

    空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。

    爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認是開啓的),用於在CMS收集器頂不住要進行Full GC時開啓內存碎片的合併整理過程,內存整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長。

    虛擬機設計者還提供了另外一個參數-XX:CMSFullGCsBeforeCompaction,它用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,即每次進入Full GC時都進行碎片整理)

G1收集器

G1收集器是一款面向服務端應用的垃圾收集器。與其它GC收集器相比,G1具備如下特點:

  • 併發和並行:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop The World停頓的時間,部分其它收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程序繼續執行
  • 分代收集:雖然G1可以不需要其它收集器配合就能獨立管理整個GC堆,但G1中仍然保留了分代的概念,它能夠採用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象
  • 空間整合:G1從整體上看是基於“標記-整理”算法實現的,從局部上看是基於“複製”算法實現的,但無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。
  • 可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java的垃圾收集器的特徵了

在G1之前的其它收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存佈局就與其它收集器有很大差別,他將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合

G1之所以能建立可預測的停頓時間模型,是因爲它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region

在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對reference類型的數據進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處於不同的Region之中(在分代中就是檢查是否老年代中的對象引用了新生代的對象),如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,在GC根結點的枚舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏

如果不計算維護Remembered Set的操作,G1收集器的運作大致可劃分爲以下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記
  • 篩選回收

初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS(Next Top at Mark Start)的值,讓下一個階段用戶程序併發運行時,能在正確可用的Region中創建新對象,這階段需要停頓線程,但耗時很短。

併發標記是從GC Roots開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。

最終標記是爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到Remembered Set中,這階段需要停頓線程,但是可並行執行。

篩選回收首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。

以下是G1收集器的執行流程:

G1收集器

垃圾收集器參數總結

參數 描述
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的收集器組合進行內存回收
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收集器時生效
UseCMSConpactAtFullCollection 設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片整理。僅在使用CMS收集器時生效
CMSFullGCsBeforeCompaction 設置CMS收集器在進行若干次垃圾收集後再啓動一次內存碎片整理。僅在使用CMS收集器時生效
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章