天天看點

垃圾收集器知識點彙總

概述

如果說收集算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實作。java虛拟機規範中對垃圾收集器應該如何實作并沒有任何規定,是以不同的廠商、不同版本的虛拟機所提供的垃圾收集器可能會有很大差别,并且一般都會提供參數供使用者根據自己的應用特點和要求組合出各個年代所使用的收集器。這裡讨論的收集器基于jdk1.7update 14之後的HotSpot虛拟機,這個虛拟機包含的收集器如圖3-5所示

垃圾收集器知識點彙總

上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。虛拟機所處的區域,則代表它是屬于新生代收集器還是老年代收集器。

1、Serial收集器

Serial收集器是最基本、發展曆史最悠久的收集器,曾經(在jdk 1.3.1之前)是虛拟機新生代收集的唯一選擇。大家看名字就會知道,這個收集器是一個單線程的收集器,但它的“單線程”的意義并不僅僅說明它隻會使用一個cpu或一條收集線程去完成垃圾收集工作,更重要的是他在進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。

垃圾收集器知識點彙總

從jdk1.3開始,一直到jdk1.7,HotSpot虛拟機開發團隊為消除後者減少工作線程因記憶體回收而導緻停頓的努力一直在進行着,從Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成果Garbage First(G1)收集器,我們看到了一個個越來越優秀的收集器,使用者線程停頓時間在不斷縮短,但是仍然沒有辦法完全消除(這裡暫不包括RTSJ中的收集器)。

雖然Serial收集器看起來像是雞肋了,但實際上到現在為止,它仍然是虛拟機運作在Client模式下的預設新生代收集器。它也有優于其他收集器的地方:簡單而高效(與其他收集器的單線程相比),對于限定單個CPU的環境來說,Serial收集器由于沒有線程互動的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

2、ParNew收集器

parNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshol、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象配置設定規則、回收政策等都與Serial收集器完全一樣,在實作上,這兩種收集器也共用了相當多的代碼。parNew收集器的工作過程:

垃圾收集器知識點彙總

parNew收集器是許多運作在server模式下的虛拟機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前隻有它能與CMS收集器配合工作。在jdk1.5時期,HotSpot推出了一款在強互動應用中幾乎可認為有劃時代意義的垃圾收集器——CMS收集器,這款收集器是HotSpot虛拟機中第一款真正意義上的并發收集器,它第一次實作了讓垃圾收集線程與使用者線程(基本上)同時工作。打個比方就是在你媽媽打掃房間的同時你還能一邊往地上扔紙屑。

3、Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複制算法的收集器,又是并行的多線程收集器…看上去和parNew都一樣,那它有什麼特别之處呢?

Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時使用者線程的停頓時間,而Parallel Scavenge收集器的目标是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運作使用者代碼的時間與CPU總消耗時間的比值,即:吞吐量 = 運作使用者代碼時間 /(運作使用者代碼時間+垃圾收集時間)。

停頓時間越短就越适合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可以高效利用CPU時間,盡快完成程式的運算任務,主要适合在背景運算而不需要太多互動的任務。

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

-XX:MaxGCPauseMillis參數

該參數允許的值是一個大于0的毫秒數,收集器盡可能地保證記憶體回收花費的時間不超過設定值。不過這裡有一個誤區:不要認為如果把這個參數的值設定的稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間是以犧牲吞吐量和新生代空間來換取的,系統把新生代調小一些,收集300M新生代肯定比收集500M快,這也直接導緻垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次,每次停頓70毫秒。停頓時間的确在下降,但吞吐量也降下來了。

-XX:GCTimeRatio參數

GCTimeRatio參數的值應當是一個大于0且小于100的整數,也就是垃圾收集時間占總時間的比率,相當于是吞吐量的倒數。如果把此參數設定為19,那允許的最大GC時間就占總時間的5%(即 1 / (1+19)),預設值為99,就是允許最大1% (即 1 / (1+99) )的垃圾收集時間。

注意:這裡的倒數一說存在異議,作者可能認為兩個數相加為1就是倒數。

由于與吞吐量關系密切,Parallel Scavenge收集器也經常被稱為“吞吐量優先”收集器。除上述兩個參數之外,Parallel Scavenge收集器還有一個參數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關參數,當這個開關參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛拟機會根據目前系統的運作情況收集性能監控資訊,動态調節這些參數以提供最适合的停頓時間或最大的吞吐量,這種調節方式稱為GC自适應的調節政策(GC Ergonomics)。自适應調節政策也是Parallel Scavenge收集器與ParNew收集器的一個重要差別。

4、Serial Old收集器

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

垃圾收集器知識點彙總

5、Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年版本,使用多線程和**“标記—整理”算法**。這個收集器是在jdk1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于比較尴尬的狀态。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别無選擇。由于老年代Serial Old收集器在服務端應用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,由于單線程的老年代收集中無法充分利用伺服器多CPU的處理能力,在老年代很大而且硬體比較進階的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。

直到Parallel Old收集器出現後,“吞吐量優先”收集器終于有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge 加 Parallel Old收集器。Parallel Old收集器的工作過程

垃圾收集器知識點彙總

6、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。目前很大一部分的java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符号這類應用的需求。

CMS收集器是基于“标記—清除”算法實作的,它的運作過程相對于前面幾種收集器來說更複雜一些,整個過程分為4個步驟,包括:

  • 初始标記(CMS initial mark)
  • 并發标記(CMS Concurrent mark)
  • 重新标記(CMS remark)
  • 并發清除(CMS Concurrent Sweep)

其中,初始标記、重新标記這兩個步驟仍然需要“stop the world”。初始标記僅僅隻是标記以下GC Roots能直接關聯到的對象,速度很快,并發标記階段就是進行GC Roots Tracing的過程,而重新标記階段則是為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段稍長一些,但遠比并發标記的時間短。

由于整個過程中耗時最長的并發标記和并發清除過程收集線程都可以與使用者線程一起工作,是以,從總體上來說,CMS收集器的記憶體回收過程是與使用者線程一起并發執行的。

垃圾收集器知識點彙總

CMS的優點

并發收集、低停頓,Sun公司的一些官方文檔中也稱為并發低停頓收集器(Concurrent Low Pause Collector)

CMS缺點

  • CMS收集器對CPU資源非常敏感。其實,面向并發設計的程式都對CPU資源比較敏感。在并發階段,它雖然不會導緻使用者線程停頓,但是會因為占用了一部分線程(或者說CPU資源)而導緻應用程式變慢,總吞吐量降低。
  • CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現**“Concurrent Mode Failure”**失敗而導緻另一次Full GC的産生。由于CMS并發清理階段使用者線程還在運作着,伴随程式運作自然就還會有新的垃圾不斷産生,這一部分垃圾出現在标記過程之後,CMS無法在當次收集中處理它們,隻好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。注意:要是CMS運作期間預留的記憶體無法滿足程式需要,就會出現“Concurrent Mode Failure”失敗,這時虛拟機将啟動後備預案:臨時啟用Serial Old收集器來重新繼續進行老年代的垃圾收集,這樣停頓時間就很長了。是以說參數-XX:CMSInitiatingOccupancyFraction設定得太高容易導緻大量“Concurrent Mode Failure”失敗,性能反而降低。
  • CMS是一款基于“标記—清除”算法實作的收集器,收集結束時會有大量空間碎片産生。空間碎片過多時,将會給大對象配置設定帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來配置設定目前對象,不得不提前觸發一次Full GC。

7、G1收集器

G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是未來可以替換掉jdk1.5中釋出的CMS收集器。與其他GC收集器相比,G1具有如下特點:

  • 并行與并發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短stop-the-world停頓的時間,部分其他收集器原本需要停頓java線程執行gc動作,G1收集器仍然可以通過并發的方式讓java程式繼續執行,
  • 分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新建立的對象和已存活了一段時間、熬過多次GC的舊對象以獲得更好的收集效果。
  • 空間整合:與CMS的“标記—清理”算法不同,G1從整體來看是基于“标記—整理”算法實作的收集器,從局部(兩個Region之間)上來看是基于“複制”算法實作的,但無論如何,這兩種算法都意味着G1運作期間不會産生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利于程式長時間運作,配置設定大對象時不會因為無法找到連續記憶體空間而提前觸發下一次GC。
  • 可預測的停頓:這是G1相對于CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1住了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時java(RTSJ)的垃圾收集器的特征了

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

G1收集器之是以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得空間大小以及回收所需時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分記憶體空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間内可以擷取盡可能高的的收集效率。

在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛拟機都是使用Remembered Set,虛拟機發現程式在對Reference類型的資料進行寫操作時,會産生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過CardTable把相關引用資訊記錄到被引用對象所屬的Region的Remembered Set之中。當進行記憶體回收時,在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。

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

  • 初始标記
  • 并發标記
  • 最終标記
  • 篩選回收

G1的前幾個步驟的運作過程和CMS有很多相似之處。初始标記階段僅僅隻是标記一下GC Roots能直接關聯到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式并發運作時,能在正确可用的Region中建立新對象,這階段需要停頓線程,但耗時很短。并發标記階段是從GC Roots開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與使用者程式并發執行。而最終标記階段則是為了修正在并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分标記記錄,虛拟機将這段時間對象變化線上程Remembered Set Logs裡面,最終标記階段需要把Remembered Set Logs的資料合并到Remembered Set中,這階段需要停頓線程,但是可并行執行。最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,從Sun公司透露出來的資訊來看,這個階段其實也可以做到與使用者程式一起并發執行,但是因為隻回收一部分Region,時間是使用者可控制的,而且停頓使用者線程将大幅度提高收集效率。下圖可以比較清除地看到G1收集器的運作步驟中并發和需要停頓的階段。

垃圾收集器知識點彙總

垃圾收集器參數總結
垃圾收集器知識點彙總
垃圾收集器知識點彙總

–本文知識點均摘自《深入了解java虛拟機》