學習java一定要知道的垃圾收集器

垃圾收集器如何演化的?

垃圾收集器的發展路線,簡單來說是隨著內存越來越大而發生變化。

從分代算法逐漸演化為不分代算法。

從serial的幾十兆,逐漸演化到parallel的幾個G,再到CMS的幾十個G,也從此開始瞭並發回收。

年輕代收集器

Serial

特點:年輕代、串行回收、STW、簡單高效

Serial(串行)收集器是最基本、發展歷史最悠久的收集器,它是采用復制算法的新生代收集器,曾經(JDK 1.3.1之前)是虛擬機新生代收集的唯一選擇。

在垃圾回收時,必須暫停其他所有線程的工作線程,即所謂的“Stop The World”。

Serial收集器是HotSpot虛擬機運行在Client模式下的默認新生代收集器。

Serial收集器具有簡單而高效,由於沒有線程交互的開銷,可以獲得最高的單線程收集效率(在單個CPU環境中)。

添加該參數來顯式的使用Serial垃圾收集器:

-XX:+UseSerialGC

ParNew

特點:年輕代、多線程回收、STW、配合CMS、核心越多效率越高

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

目前隻有它能和CMS收集器(Concurrent Mark Sweep)配合工作。

在單CPU的環境下,其性能絕對不會有比Serial收集器有更好的效果,甚至由於多線程的切換開銷,在雙核情況下也有可能無法做到超越Serial。 在多核的情況下,GC線程數默認與核心數相同,隨著核心的增多,能獲得更好的效果。

指定使用CMS後,會默認使用ParNew作為新生代收集器:

-XX:+UseConcMarkSweepGC

強制指定使用ParNew

-XX:+UseParNewGC

指定垃圾收集的線程數量,ParNew默認開啟的收集線程與CPU的數量相同:

-XX:ParallelGCThreads

Parallel Scavenge

特點:年輕代、並行回收、吞吐量、GC自適應的調節策略(GC Ergonomics)

Parallel Scavenge收集器是一個新生代收集器,使用復制算法,且是並行的多線程收集器。

Parallel Scavenge收集器關註點是達到一個可控制的吞吐量:

(吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間))

而其他收集器關註點在盡可能的縮短垃圾收集時用戶線程的停頓時間。

控制最大垃圾收集停頓時間:參數允許的值是一個大於0的毫秒數,收集器將盡可能的保證內存垃圾回收花費的時間不超過設定的值(但是,並不是越小越好,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,如果設置的值太小,將會導致頻繁GC,這樣雖然GC停頓時間下來瞭,但是吞吐量也下來瞭)

-XX:MaxGCPauseMillis

控制吞吐量大小:參數的值是一個大於0且小於100的整數,也就是垃圾收集時間占總時間的比率,默認值是99,就是允許最大1%(即1/(1+99))的垃圾收集時間。

-XX:GCTimeRatio

GC自適應的調節策略(GC Ergonomics) :當這個參數打開之後,就不需要手工指定新生代的大小、Eden與Survivor區的比例、晉升老年代對象年齡等細節參數瞭,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics)。隻需要把基本的內存數據設置好(如-Xmx設置最大堆),然後使用MaxGVPauseMillis參數或GCTimeRation參數給虛擬機設立一個優化目標,JVM會自動調節其他優化參數

-XX:+UseAdaptiveSizePolicy

老年代收集器

SerialOld

特點:老年代、串行回收、STW

Serial Old收集器是Seria收集器的老年代版本,他同樣是一個單線程收集器,使用" 標記-整理" 算法。

Serial Old收集器主要用於Client模式下的虛擬機使用。

Server模式下的兩大用途:

  • 在JDK1.5及之前的版本與Parallel Scavenge收集器搭配使用;
  • 作為CMS收集器的後備方案,在並發收集發生Conturrent Mode Failure時使用。

結合Serial的回收流程如下圖所示:

ParallelOld

特點:老年代、多線程、STW、吞吐量

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。此收集器的出現代表著“吞吐量優先”收集器終於有瞭比較名副其實的應用組合,在註重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。

使用po收集參數:

-XX:+UseParallelOldGC

與Parallele Scavenge結合使用的回收流程如下:

使用吞吐量收集器的吞吐量計算方式如下圖所示:

CMS(ConcurrentMarkSweep)

特點:老年代、並發的,短停頓(降低STW時間)

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,它非常符合那些集中在互聯網站或者B/S系統的服務端上的Java應用,這些應用都非常重視服務的響應速度。從名字上(“Mark Sweep”)就可以看出它是基於“標記-清除”算法實現的。

CMS問題比較多,所以現在沒有一個版本默認是CMS,隻能手工指定。

CMS既然是MarkSweep,就一定會有碎片化的問題,碎片到達一定程度,CMS的老年代分配對象分配不下的時候,使用SerialOld 進行老年代回收。

CMS的回收過程主要分為4個步驟:

  • 初始標記 標記一下GC Roots能直接關聯到的對象,速度很快,需要“Stop The World”。
  • 並發標記 進行GC Roots Tracing(根可達算法)的過程,在整個過程中耗時最長,標記所有根節點能照的對象。與用戶線程一起工作。
  • 重新標記 為瞭修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。此階段也需要“Stop The World”。
  • 並發清理 與用戶線程一起運行。

從以上過程中可以總結出其優點:並發收集,並發清理,地停頓。

三個缺點:

  • CMS收集器對CPU資源非常敏感。 CMS默認啟動的回收線程數是(CPU數量+3)/4,回收線程會占用部分線程資源,導致系統吞吐量降低。在選用CMS收集器的時候,需要考慮,當前的應用系統,是否對CPU資源敏感。
  • 垃圾收集過程中,無法處理浮動垃圾,可能會出現Concurrent Mode Failure問題,導致觸發Full GC。
  • 浮動垃圾:是由於CMS收集器的並發清理階段,清理線程是和用戶線程一起運行,如果在清理過程中,用戶線程產生瞭垃圾對象,由於過瞭標記階段,所以這些垃圾對象就成為瞭浮動垃圾。
  • CMS無法在當前垃圾收集過程中集中處理這些浮動垃圾,需要預留內存空間去保存這些垃圾,可以通過參數 -XX:CMSInitiatingOccupancyFraction 控制觸發占用老年代的百分比。
  • 如果預留空間無法裝下浮動垃圾,會導致Concurrent Mode Failure失敗,這個時候,虛擬機將啟動後備預案,臨時啟動Serial Old收集器來對老年代重新進行垃圾收集,這樣會導致垃圾收集的時間邊長,特別是當老年代內存很大的時候。所以對參數-XX:CMSInitiatingOccupancyFraction的設置,過高,會導致發生Concurrent Mode Failure,過低,則浪費內存空間。
  • 使用的"標記-清除"算法會出現很多內存碎片。 過多的內存碎片導致缺少連續的內存空間,會影響大對象的分配,最終觸發Full GC,為瞭解決這個問題,CMS收集器提供瞭一個"-XX:+UseCMSCompactAtFullCollection"參數(默認是開啟的),用於CMS收集器在必要的時候對內存碎片進行壓縮整理。由於內存碎片整理過程不是並發的,所以會導致停頓時間變長。虛擬機還提供瞭一個-XX:CMSFullGCsBeforeCompaction"參數,來控制進行過多少次不壓縮的Full GC以後,進行一次帶壓縮的Full GC,默認值是0,表示每次在進行Full GC前都進行碎片整理。

主要參數: 使用CMS收集器:

-XX:+UseConcMarkSweepGC

Full GC後,進行一次碎片整理;整理過程是獨占的,會引起停頓時間變長:

-XX:+UseCMSCompactAtFullCollection

設置進行幾次Full GC後,進行一次碎片整理:

-XX:+CMSFullGCsBeforeCompaction

設定CMS的線程數量(一般情況約等於可用CPU數量):

-XX:ParallelCMSThreads

新型收集器

G1

算法:三色標記 + SATB

G1(Garbage-First)收集器是當今收集器技術發展最前沿的成果之一,它是一款面向服務端應用的垃圾收集器,HotSpot開發團隊賦予它的使命是(在比較長期的)未來可以替換掉JDK 1.5中發佈的CMS收集器。G1從JDK9開始已經作為默認的垃圾回收器。如果對於應用程序來說停頓時間比吞吐量更重要,G1是非常合適的選擇。

G1與上面介紹的GC有很大的不同

  • G1的設計原則是首先收集盡可能多的垃圾(Garbage First) 。 G1並不會等內存耗盡(串行、並行)或者快耗盡(CMS)的時候開始垃圾收集,而是在內部采用瞭啟發式算法,在老年代找出具有高收集收益的分區進行收集
  • G1采用內存分區(Region)的思路
  • 邏輯上的分代概念,不是物理分代
  • 所有收集都是STW,混合(mixed)收集(年輕代和老年代同時進行收集)

三色標記法 說起並發回收,就不得不瞭解三色標記法。先學習三色標記法,更好的理解G1的分區模型。

我們將對象分成三種類型:

  • 黑色:跟對象或者該對象與它的子對象都被掃描(標記完成)。
  • 灰色:對象本身被標記完成,但是還沒有掃描完該對象中的子對象
  • 白色:未被掃描的對象。或者是掃描完成後,最終白色對象為不可達對象即為垃圾對象。

其實現過程如下圖所示:

  • 第一步:根對象被置為黑色,子節點置為灰色。
  • 第二步:繼續遍歷灰色對象,將遍歷過的子節點置為灰色,當前灰節點置為黑色。
  • 第三部:遍歷所有可達對象後,所有可達對象置為黑色,不可達的為白色,將要被回收。

三色標記存在的問題 如下圖所示,如果gc掃描到左側位置,這時候程序執行C.d = D,B.d = null,則會導致B對D的引用消失,而C則引用瞭D,可是此時C是黑色的,不會對其子節點進行掃描,則D節點會被當做垃圾進行回收。

三色標記法問題解決方案

通常有兩種解決方案:

  • 在插入的時候記錄對象(CMS增量更新)
  • 在刪除的時候記錄對象(G1 STAB)

增量更新(Incremental update) : 在CMS采用的是增量更新(Incremental update),隻要在寫屏障(write barrier)裡發現要有一個白對象的引用被賦值到一個黑對象 的字段裡,那就把這個白對象變成灰色的。即插入的時候記錄下來。

STAB(snapshot-at-the-beginning) : STAB 的做法在 GC 開始時,在初始標記時對內存進行一個對象圖的邏輯快照 (snapshot),隻要被快照到對象是活的,那在整個 GC 的過程中對象就被認定的是活的,即使該對象的引用稍後被修改或者刪除。

同時新分配的對象也會被認為是活的,除此之外其它不可達的對象就被認為是死掉瞭。這樣 STAB 就保證瞭真正存活的對象不會被 GC 誤回收,但同時也造成瞭某些可以被回收的對象逃過瞭 GC,導致瞭內存裡面存在浮動的垃圾 (float garbage),這些浮動垃圾將在下次回收時被收集。

G1的分區

G1是邏輯分代,將堆分為若幹個區域(region),新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另外一個區域,完成瞭清理工作。這樣就解決瞭CMS中的內存碎片問題。

如上圖分代模型所示,G1仍然有Eden,Survivor,Old等區域,同時多瞭一個Humongous Region(巨型對象)。

巨型對象:一個大小達到甚至超過分區大小一半的對象稱為巨型對象(Humongous Object)。

巨型對象:”會獨占一個、或多個連續分區,其中第一個分區被標記為開始巨型(StartsHumongous),相鄰連續分區被標記為連續巨型(ContinuesHumongous)。巨型對象會直接在老年代分配,所占用的連續空間稱為巨型分區(Humongous Region)。G1內部做瞭一個優化,一旦發現沒有引用指向巨型對象,則可直接在年輕代收集周期中被回收。

G1中GC的分類:

  • Young GC:主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。 在這種情況下,Eden空間的數據移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數據會直接晉升到年老代空間。Survivor區的數據移動到新的Survivor區中,也有部分數據晉升到老年代空間中。最終Eden空間的數據為空,GC停止工作,應用線程繼續執行
  • Mix GC:不僅進行正常的新生代垃圾收集,同時也回收部分後臺掃描線程標記的老年代分區。

G1是否有Full GC? 答案是肯定的,G1觸發瞭Full GC,這時G1會退化使用Serial Old收集器來完成垃圾的清理工作。

Full GC產生的原因? G1在對象復制/轉移失敗或者沒法分配足夠內存(比如巨型對象沒有足夠的連續分區分配)時,會觸發Full GC。Full GC使用的是stop the world的單線程的Serial Old模式,所以一旦觸發Full GC則會STW應用線程,並且執行效率很慢。JDK 8版本的G1是不提供Full GC的處理的。對於G1 GC的優化,很大的目標就是沒有Full GC。

剩下三種GC本文會在後面主鍵補充:

  • ZGC (10ms – 1ms) PK C++:算法:ColoredPointers + LoadBarrier
  • Shenandoah:算法:ColoredPointers + WriteBarrier
  • Eplison:Epsilon不回收內存,隻負責堆的管理與佈局,對象的分配,與解釋器、編譯器、監控子系統的協作; epsilon適合運行時間短、在內存耗盡前就可退出的應用程序

到此這篇關於學習java一定要知道的垃圾收集器的文章就介紹到這瞭,更多相關java垃圾收集器內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: