如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)。
Java虛擬機規(guī)范中對垃圾收集器應(yīng)該如何實現(xiàn)并沒有任何規(guī)定,因此不同的廠商、版本的虛擬機所提供的垃圾收集器都可能會有很大差別,并且一般都會提供參數(shù)供用戶根據(jù)自己的應(yīng)用特點和要求組合出各個年代所使用的收集器。
接下來討論的收集器基于JDK1.7 Update 14 之后的HotSpot虛擬機(在此版本中正式提供了商用的G1收集器,之前G1仍處于實驗狀態(tài)),該虛擬機包含的所有收集器如下圖所示:

上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。
虛擬機所處的區(qū)域,則表示它是屬于新生代收集器還是老年代收集器。Hotspot實現(xiàn)了如此多的收集器,正是因為目前并無完美的收集器出現(xiàn),只是選擇對具體應(yīng)用最適合的收集器。
相關(guān)概念
并行和并發(fā)
- 并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態(tài)。
- 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時執(zhí)行(但不一定是并行的,可能會交替執(zhí)行),用戶程序在繼續(xù)運行。而垃圾收集程序運行在另一個CPU上。
吞吐量(Throughput)
吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即
吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。
假設(shè)虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
Minor GC 和 Full GC
- 新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。具體原理見上一篇文章。
- 老年代GC(Major GC / Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC,經(jīng)常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。
新生代收集器
Serial收集器
Serial(串行)收集器是最基本、發(fā)展歷史最悠久的收集器,它是采用復(fù)制算法的新生代收集器,曾經(jīng)(JDK 1.3.1之前)是虛擬機新生代收集的唯一選擇。
它是一個單線程收集器,只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程,直至Serial收集器收集結(jié)束為止(“Stop The World”)。
這項工作是由虛擬機在后臺自動發(fā)起和自動完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應(yīng)用來說是難以接收的。圖解 Java 垃圾回收機制,這篇推薦看下。
下圖展示了Serial 收集器(老年代采用Serial Old收集器)的運行過程:

為了消除或減少工作線程因內(nèi)存回收而導(dǎo)致的停頓,HotSpot虛擬機開發(fā)團隊在JDK 1.3之后的Java發(fā)展歷程中研發(fā)出了各種其他的優(yōu)秀收集器,這些將在稍后介紹。但是這些收集器的誕生并不意味著Serial收集器已經(jīng)“老而無用”,實際上到現(xiàn)在為止,它依然是HotSpot虛擬機運行在Client模式下的默認的新生代收集器。
它也有著優(yōu)于其他收集器的地方:簡單而高效(與其他收集器的單線程相比),對于限定單個CPU的環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得更高的單線程收集效率。
在用戶的桌面應(yīng)用場景中,分配給虛擬機管理的內(nèi)存一般不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的內(nèi)存,桌面應(yīng)用基本不會再大了),停頓時間完全可以控制在幾十毫秒最多一百毫秒以內(nèi),只要不頻繁發(fā)生,這點停頓時間可以接收。
所以,Serial收集器對于運行在Client模式下的虛擬機來說是一個很好的選擇。
ParNew 收集器
ParNew收集器就是Serial收集器的多線程版本,它也是一個新生代收集器。除了使用多線程進行垃圾收集外,其余行為包括Serial收集器可用的所有控制參數(shù)、收集算法(復(fù)制算法)、Stop The World、對象分配規(guī)則、回收策略等與Serial收集器完全相同,兩者共用了相當多的代碼。
ParNew收集器的工作過程如下圖(老年代采用Serial Old收集器):

ParNew收集器除了使用多線程收集外,其他與Serial收集器相比并無太多創(chuàng)新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關(guān)的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一個具有劃時代意義的收集器,具體內(nèi)容將在稍后進行介紹。
ParNew 收集器在單CPU的環(huán)境中絕對不會有比Serial收集器有更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程技術(shù)實現(xiàn)的兩個CPU的環(huán)境中都不能百分之百地保證可以超越。
在多CPU環(huán)境下,隨著CPU的數(shù)量增加,它對于GC時系統(tǒng)資源的有效利用是很有好處的。它默認開啟的收集線程數(shù)與CPU的數(shù)量相同,在CPU非常多的情況下可使用-XX:ParallerGCThreads參數(shù)設(shè)置。
Parallel Scavenge 收集器
Parallel Scavenge收集器也是一個并行的多線程新生代收集器,它也使用復(fù)制算法。Parallel Scavenge收集器的特點是它的關(guān)注點與其他收集器不同,CMS等收集器的關(guān)注點是盡可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標是達到一個可控制的吞吐量(Throughput)。
停頓時間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗。而高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務(wù),主要適合在后臺運算而不需要太多交互的任務(wù)。
Parallel Scavenge收集器除了會顯而易見地提供可以精確控制吞吐量的參數(shù),還提供了一個參數(shù)-XX:+UseAdaptiveSizePolicy,這是一個開關(guān)參數(shù),打開參數(shù)后,就不需要手工指定新生代的大?。?Xmn)、Eden和Survivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節(jié)參數(shù)了。
虛擬機會根據(jù)當前系統(tǒng)的運行情況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量,這種方式稱為GC自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics)。自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區(qū)別。
另外值得注意的一點是,Parallel Scavenge收集器無法與CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代選擇Parallel Scavenge收集器,老年代只有Serial Old收集器能與之配合使用。
老年代收集器
Serial Old收集器
Serial Old 是 Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”(Mark-Compact)算法。
此收集器的主要意義也是在于給Client模式下的虛擬機使用。如果在Server模式下,它還有兩大用途:
- 在JDK1.5 以及之前版本(Parallel Old誕生以前)中與Parallel Scavenge收集器搭配使用。
- 作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用。
它的工作流程與Serial收集器相同,這里再次給出Serial/Serial Old配合使用的工作流程圖:

Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。前面已經(jīng)提到過,這個收集器是在JDK 1.6中才開始提供的,在此之前,如果新生代選擇了Parallel Scavenge收集器。
老年代除了Serial Old以外別無選擇,所以在Parallel Old誕生以后,“吞吐量優(yōu)先”收集器終于有了比較名副其實的應(yīng)用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器。
Parallel Old收集器的工作流程與Parallel Scavenge相同,這里給出Parallel Scavenge/Parallel Old收集器配合使用的流程圖:

CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,它非常符合那些集中在互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端上的Java應(yīng)用,這些應(yīng)用都非常重視服務(wù)的響應(yīng)速度。從名字上(“Mark Sweep”)就可以看出它是基于“標記-清除”算法實現(xiàn)的。
CMS收集器工作的整個流程分為以下4個步驟:
- 初始標記(CMS initial mark):僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快,需要“Stop The World”。
- 并發(fā)標記(CMS concurrent mark):進行GC Roots Tracing的過程,在整個過程中耗時最長。
- 重新標記(CMS remark):為了修正并發(fā)標記期間因用戶程序繼續(xù)運作而導(dǎo)致標記產(chǎn)生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發(fā)標記的時間短。此階段也需要“Stop The World”。
- 并發(fā)清除(CMS concurrent sweep)
由于整個過程中耗時最長的并發(fā)標記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作。
所以,從總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。通過下圖可以比較清楚地看到CMS收集器的運作步驟中并發(fā)和需要停頓的時間:

優(yōu)點
CMS是一款優(yōu)秀的收集器,它的主要優(yōu)點在名字上已經(jīng)體現(xiàn)出來了:并發(fā)收集、低停頓,因此CMS收集器也被稱為并發(fā)低停頓收集器(Concurrent Low Pause Collector)。
缺點
- 對CPU資源非常敏感 其實,面向并發(fā)設(shè)計的程序都對CPU資源比較敏感。在并發(fā)階段,它雖然不會導(dǎo)致用戶線程停頓,但會因為占用了一部分線程(或者說CPU資源)而導(dǎo)致應(yīng)用程序變慢,總吞吐量會降低。
- CMS默認啟動的回收線程數(shù)是(CPU數(shù)量+3)/4,也就是當CPU在4個以上時,并發(fā)回收時垃圾收集線程不少于25%的CPU資源,并且隨著CPU數(shù)量的增加而下降。但是當CPU不足4個時(比如2個),CMS對用戶程序的影響就可能變得很大,如果本來CPU負載就比較大,還要分出一半的運算能力去執(zhí)行收集器線程,就可能導(dǎo)致用戶程序的執(zhí)行速度忽然降低了50%,其實也讓人無法接受。
- 無法處理浮動垃圾(Floating Garbage) 可能出現(xiàn)“Concurrent Mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生。
- 由于CMS并發(fā)清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產(chǎn)生。這一部分垃圾出現(xiàn)在標記過程之后,CMS無法再當次收集中處理掉它們,只好留待下一次GC時再清理掉。
- 這一部分垃圾就被稱為“浮動垃圾”。也是由于在垃圾收集階段用戶線程還需要運行,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預(yù)留一部分空間提供并發(fā)收集時的程序運作使用。
- 標記-清除算法導(dǎo)致的空間碎片 CMS是一款基于“標記-清除”算法實現(xiàn)的收集器,這意味著收集結(jié)束時會有大量空間碎片產(chǎn)生。
- 空間碎片過多時,將會給大對象分配帶來很大麻煩,往往出現(xiàn)老年代空間剩余,但無法找到足夠大連續(xù)空間來分配當前對象。
G1收集器
G1(Garbage-First)收集器是當今收集器技術(shù)發(fā)展最前沿的成果之一,它是一款面向服務(wù)端應(yīng)用的垃圾收集器,HotSpot開發(fā)團隊賦予它的使命是(在比較長期的)未來可以替換掉JDK 1.5中發(fā)布的CMS收集器。與其他GC收集器相比,G1具備如下特點:
- 并行與并發(fā) G1 能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU來縮短“Stop The World”停頓時間,部分其他收集器原本需要停頓Java線程執(zhí)行的GC動作,G1收集器仍然可以通過并發(fā)的方式讓Java程序繼續(xù)執(zhí)行。
- 分代收集 與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同方式去處理新創(chuàng)建的對象和已存活一段時間、熬過多次GC的舊對象來獲取更好的收集效果。
- 空間整合 G1從整體來看是基于“標記-整理”算法實現(xiàn)的收集器,從局部(兩個Region之間)上來看是基于“復(fù)制”算法實現(xiàn)的。這意味著G1運行期間不會產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。此特性有利于程序長時間運行,分配大對象時不會因為無法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC。
- 可預(yù)測的停頓 這是G1相對CMS的一大優(yōu)勢,降低停頓時間是G1和CMS共同的關(guān)注點,但G1除了降低停頓外,還能建立可預(yù)測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在GC上的時間不得超過N毫秒,這幾乎已經(jīng)是實時Java(RTSJ)的垃圾收集器的特征了。
橫跨整個堆內(nèi)存
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老生代,而G1不再是這樣。對象都是在堆上分配的嗎?推薦大家看下。關(guān)注微信公眾號:Java技術(shù)棧,在后臺回復(fù):JVM,可以獲取我整理的 N 篇最新 JVM 教程,都是干貨。
G1在使用時,Java堆的內(nèi)存布局與其他收集器有很大區(qū)別,它將整個Java堆劃分為多個大小相等的獨立區(qū)域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,而都是一部分Region(不需要連續(xù))的集合。
建立可預(yù)測的時間模型
G1收集器之所以能建立可預(yù)測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區(qū)域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限的時間內(nèi)可以獲取盡可能高的收集效率。
避免全堆掃描——Remembered Set
G1把Java堆分為多個Region,就是“化整為零”。但是Region不可能是孤立的,一個對象分配在某個Region中,可以與整個Java堆任意的對象發(fā)生引用關(guān)系。在做可達性分析確定對象是否存活的時候,需要掃描整個Java堆才能保證準確性,這顯然是對GC效率的極大傷害。
為了避免全堆掃描的發(fā)生,虛擬機為G1中每個Region維護了一個與之對應(yīng)的Remembered Set。虛擬機發(fā)現(xiàn)程序在對Reference類型的數(shù)據(jù)進行寫操作時,會產(chǎn)生一個Write Barrier暫時中斷寫操作。
檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過CardTable把相關(guān)引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內(nèi)存回收時,在GC根節(jié)點的枚舉范圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。
如果不計算維護Remembered Set的操作,G1收集器的運作大致可劃分為以下幾個步驟:
- 初始標記(Initial Marking) 僅僅只是標記一下GC Roots 能直接關(guān)聯(lián)到的對象,并且修改TAMS(Nest Top Mark Start)的值,讓下一階段用戶程序并發(fā)運行時,能在正確可以的Region中創(chuàng)建對象,此階段需要停頓線程,但耗時很短。
- 并發(fā)標記(Concurrent Marking) 從GC Root 開始對堆中對象進行可達性分析,找到存活對象,此階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。
- 最終標記(Final Marking) 為了修正在并發(fā)標記期間因用戶程序繼續(xù)運作而導(dǎo)致標記產(chǎn)生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的Remembered Set Logs里面,最終標記階段需要把Remembered Set Logs的數(shù)據(jù)合并到Remembered Set中,這階段需要停頓線程,但是可并行執(zhí)行。
- 篩選回收(Live Data Counting and Evacuation) 首先對各個Region中的回收價值和成本進行排序,根據(jù)用戶所期望的GC 停頓是時間來制定回收計劃。此階段其實也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因為只回收一部分Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。
通過下圖可以比較清楚地看到G1收集器的運作步驟中并發(fā)和需要停頓的階段(Safepoint處):

總結(jié)
