????????如果說收集算法是內存回收的方法論,垃圾收集器就是內存回收的具體實現。Java虛擬機規(guī)范中對垃圾收集器應該如何實現并沒有任何規(guī)定,因此不同的廠商、不同版本的虛擬機所提供的垃圾收集器都可能會有很大的差別,并且一般都會提供參數供用戶根據自己的應用特點和要求組合出各個年代所使用的收集器。這里討論的收集器基于Sun HotSpot虛擬機1.6版 Update 22。


????????雖然我們是在對各個收集器進行比較,但并非為了挑選一個最好的收集器出來。因為直到現在為止還沒有最好的收集器出現,更加沒有萬能的收集器,所以我們選擇的只是對具體應用最合適的收集器。
Serial收集器
? ??????Serial收集器是最基本、歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是虛擬機新生代收集的唯一選擇。大家看名字就知道,這個收集器是一個單線程的收集器,但它的“單線程”的意義并不僅僅是說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程(Sun將這件事情稱之為“Stop The World”),直到它收集結束?!癝top The World”這個名字也許聽起來很酷,但這項工作實際上是由虛擬機在后臺自動發(fā)起和自動完成的,在用戶不可見的情況下把用戶的正常工作的線程全部停掉,這對很多應用來說都是難以接受的。你想想,要是你的電腦每運行一個小時就會暫停響應5分鐘,你會有什么樣的心情?
????????對于“Stop The World”帶給用戶的惡劣體驗,虛擬機的設計者們表示完全理解,但也表示非常委屈:“你媽媽在給你打掃房間的時候,肯定也會讓你老老實實地在椅子上或房間外待著,如果她一邊打掃,你一邊亂扔紙屑,這房間還能打掃完嗎?”這確實是一個合情合理的矛盾,雖然垃圾收集這項工作聽起來和打掃房間屬于一個性質的,但實際上肯定還要比打掃房間復雜得多啊!

????????從JDK 1.3開始,HotSpot虛擬機開發(fā)團隊為消除或減少工作線程因內存回收而導致停頓的努力一直在進行著,從Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)現在還未正式發(fā)布的Garbage First(G1)收集器,我們看到了一個個越來越優(yōu)秀(也越來越復雜)的收集器的出現,用戶線程的停頓時間在不斷縮短,但是仍然沒有辦法完全消除(這里暫不包括RTSJ中的收集器)。尋找更優(yōu)秀的垃圾收集器的工作仍在繼續(xù)!
????????寫到這里,筆者似乎已經把Serial收集器描述成一個老而無用,食之無味棄之可惜的雞肋了,但實際上到現在為止,它依然是虛擬機運行在Client模式下的默認新生代收集器。它也有著優(yōu)于其他收集器的地方:簡單而高效(與其他收集器的單線程比),對于限定單個CPU的環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。在用戶的桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的內存,桌面應用基本上不會再大了),停頓時間完全可以控制在幾十毫秒最多一百多毫秒以內,只要不是頻繁發(fā)生,這點停頓是可以接受的。所以,Serial收集器對于運行在Client模式下的虛擬機來說是一個很好的選擇。
ParNew收集器
????????ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規(guī)則、回收策略等都與Serial收集器完全一樣,實現上這兩種收集器也共用了相當多的代碼。ParNew收集器的工作過程如圖3-7所示。

????????ParNew收集器除了多線程收集之外,其他與Serial收集器相比并沒有太多創(chuàng)新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。在JDK 1.5時期,HotSpot推出了一款在強交互應用中幾乎可稱為有劃時代意義的垃圾收集器—CMS收集器(Concurrent Mark Sweep,本節(jié)稍后將詳細介紹這款收集器),這款收集器是HotSpot虛擬機中第一款真正意義上的并發(fā)(Concurrent)收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作,用前面那個例子的話來說,就是做到了在你媽媽打掃房間的時候你還能同時往地上扔紙屑。
????????不幸的是,它作為老年代的收集器,卻無法與JDK 1.4.0中已經存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或Serial收集器中的一個。ParNew收集器也是使用 -XX: +UseConcMarkSweepGC選項后的默認新生代收集器,也可以使用 -XX:+UseParNewGC選項來強制指定它。
????????ParNew收集器在單CPU的環(huán)境中絕對不會有比Serial收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環(huán)境中都不能百分之百地保證能超越Serial收集器。當然,隨著可以使用的CPU的數量的增加,它對于GC時系統(tǒng)資源的利用還是很有好處的。它默認開啟的收集線程數與CPU的數量相同,在CPU非常多(譬如32個,現在CPU動輒就4核加超線程,服務器超過32個邏輯CPU的情況越來越多了)的環(huán)境下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
? ? ? ??并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態(tài)。?
? ??????并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時執(zhí)行(但不一定是并行的,可能會交替執(zhí)行),用戶程序繼續(xù)運行,而垃圾收集程序運行于另一個CPU上。
Parallel Scavenge收集器
????????Parallel Scavenge收集器也是一個新生代收集器,它也是使用復制算法的收集器,又是并行的多線程收集器……看上去和ParNew都一樣,那它有什么特別之處呢?
????????Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
????????停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶的體驗;而高吞吐量則可以最高效率地利用CPU時間,盡快地完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。
????????Parallel Scavenge收集器提供了兩個參數用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數及直接設置吞吐量大小的 -XX:GCTimeRatio參數。
????????MaxGCPauseMillis參數允許的值是一個大于0的毫秒數,收集器將盡力保證內存回收花費的時間不超過設定值。不過大家不要異想天開地認為如果把這個參數的值設置得稍小一點就能使得系統(tǒng)的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統(tǒng)把新生代調小一些,收集300MB新生代肯定比收集500MB快吧,這也直接導致垃圾收集發(fā)生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。
????????GCTimeRatio參數的值應當是一個大于0小于100的整數,也就是垃圾收集時間占總時間的比率,相當于是吞吐量的倒數。如果把此參數設置為19,那允許的最大GC時間就占總時間的5%(即1 /(1+19)),默認值為99,就是允許最大1%(即1 /(1+99))的垃圾收集時間。
????????由于與吞吐量關系密切,Parallel Scavenge收集器也經常被稱為“吞吐量優(yōu)先”收集器。除上述兩個參數之外,Parallel Scavenge收集器還有一個參數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關參數,當這個參數打開之后,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節(jié)參數了,虛擬機會根據當前系統(tǒng)的運行情況收集性能監(jiān)控信息,動態(tài)調整這些參數以提供最合適的停頓時間或最大的吞吐量,這種調節(jié)方式稱為GC自適應的調節(jié)策略(GC Ergonomics)。如果讀者對于收集器運作原理不太了解,手工優(yōu)化存在困難的時候,使用Parallel Scavenge收集器配合自適應調節(jié)策略,把內存管理的調優(yōu)任務交給虛擬機去完成將是一個很不錯的選擇。只需要把基本的內存數據設置好(如-Xmx設置最大堆),然后使用MaxGCPauseMillis參數(更關注最大停頓時間)或GCTimeRatio參數(更關注吞吐量)給虛擬機設立一個優(yōu)化目標,那具體細節(jié)參數的調節(jié)工作就由虛擬機完成了。自適應調節(jié)策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區(qū)別。
Serial Old收集器
????????Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是被Client模式下的虛擬機使用。如果在Server模式下,它主要還有兩大用途:一個是在JDK 1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個就是作為CMS收集器的后備預案,在并發(fā)收集發(fā)生Concurrent Mode Failure的時候使用。這兩點都將在后面的內容中詳細講解。

Parallel Old收集器
? ??????
????????Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在JDK 1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態(tài)。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇(還記得上面說過Parallel Scavenge收集器無法與CMS收集器配合工作嗎?)。由于單線程的老年代Serial Old收集器在服務端應用性能上的“拖累”,即便使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,又因為老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬件比較高級的環(huán)境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。
????????直到Parallel Old收集器出現后,“吞吐量優(yōu)先”收集器終于有了比較名副其實的應用組合,在注重吞吐量及CPU資源敏感的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器。

? ??????
CMS收集器
????????CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用都集中在互聯網站或B/S系統(tǒng)的服務端上,這類應用尤其重視服務的響應速度,希望系統(tǒng)停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
????????從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“標記-清除”算法實現的,它的運作過程相對于前面幾種收集器來說要更復雜一些,整個過程分為4個步驟,包括:
a.初始標記(CMS initial mark)
b.并發(fā)標記(CMS concurrent mark)
c.重新標記(CMS remark)
d.并發(fā)清除(CMS concurrent sweep)
????????其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,并發(fā)標記階段就是進行GC Roots Tracing(可達性算法)的過程,而重新標記階段則是為了修正并發(fā)標記期間,因用戶程序繼續(xù)運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發(fā)標記的時間短。
????????由于整個過程中耗時最長的并發(fā)標記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程是與用戶線程一起并發(fā)地執(zhí)行的。通過圖3-10可以比較清楚地看到CMS收集器的運作步驟中并發(fā)和需要停頓的時間。

????????CMS是一款優(yōu)秀的收集器,它的最主要優(yōu)點在名字上已經體現出來了:并發(fā)收集、低停頓,Sun的一些官方文檔里面也稱之為并發(fā)低停頓收集器(Concurrent Low Pause Collector)。但是CMS還遠達不到完美的程度,它有以下三個顯著的缺點:
????????CMS收集器對CPU資源非常敏感。其實,面向并發(fā)設計的程序都對CPU資源比較敏感。在并發(fā)階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS默認啟動的回收線程數是(CPU數量+3)/ 4,也就是當CPU在4個以上時,并發(fā)回收時垃圾收集線程最多占用不超過25%的CPU資源。但是當CPU不足4個時(譬如2個),那么CMS對用戶程序的影響就可能變得很大,如果CPU負載本來就比較大的時候,還分出一半的運算能力去執(zhí)行收集器線程,就可能導致用戶程序的執(zhí)行速度忽然降低了50%,這也很讓人受不了。為了解決這種情況,虛擬機提供了一種稱為“增量式并發(fā)收集器”(Incremental Concurrent Mark Sweep / i-CMS)的CMS收集器變種,所做的事情和單CPU年代PC機操作系統(tǒng)使用搶占式來模擬多任務機制的思想一樣,就是在并發(fā)標記和并發(fā)清理的時候讓GC線程、用戶線程交替運行,盡量減少GC線程的獨占資源的時間,這樣整個垃圾收集的過程會更長,但對用戶程序的影響就會顯得少一些,速度下降也就沒有那么明顯,但是目前版本中,i-CMS已經被聲明為“deprecated”,即不再提倡用戶使用。
????????CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由于CMS并發(fā)清理階段用戶線程還在運行著,伴隨程序的運行自然還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在本次收集中處理掉它們,只好留待下一次GC時再將其清理掉。這一部分垃圾就稱為“浮動垃圾”。也是由于在垃圾收集階段用戶線程還需要運行,即還需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發(fā)收集時的程序運作使用。在默認設置下,CMS收集器在老年代使用了68%的空間后就會被激活,這是一個偏保守的設置,如果在應用中老年代增長不是太快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發(fā)百分比,以便降低內存回收次數以獲取更好的性能。要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時候虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiatingOccupancyFraction設置得太高將會很容易導致大量“Concurrent Mode Failure”失敗,性能反而降低。
????????還有最后一個缺點,在本節(jié)在開頭說過,CMS是一款基于“標記-清除”算法實現的收集器,如果讀者對前面這種算法介紹還有印象的話,就可能想到這意味著收集結束時會產生大量空間碎片。空間碎片過多時,將會給大對象分配帶來很大的麻煩,往往會出現老年代還有很大的空間剩余,但是無法找到足夠大的連續(xù)空間來分配當前對象,不得不提前觸發(fā)一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數,用于在“享受”完Full GC服務之后額外免費附送一個碎片整理過程,內存整理的過程是無法并發(fā)的??臻g碎片問題沒有了,但停頓時間不得不變長了。虛擬機設計者們還提供了另外一個參數-XX: CMSFullGCsBeforeCompaction,這個參數用于設置在執(zhí)行多少次不壓縮的Full GC后,跟著來一次帶壓縮的。
G1收集器
G1是一款面向服務端應用的垃圾收集器具備如下特點:
?a.并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時問,部分其他收集器原本需要停頓 Java線程執(zhí)行的GC動作,G1收集器仍然可以通過并發(fā)的方式讓Java程序繼續(xù)執(zhí)行。
b.分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創(chuàng)建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。
c.空間整合:與CMS的“標記一清理”算法不同,G1從整休來看是基于“標記—整理”算法實現的收集器,從局部(兩個Region之間)上來看是基于“復制”算法實現的, 但無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片,收集后能提供規(guī)整的可用內存。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續(xù)內存空間而提前觸發(fā)下一次GC。
d.可預測的停頓:這是G1相對于CMS的另一大優(yōu)勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M亳秒的時間片段內,消耗在垃圾收集上的時間不得超過N亳秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。
? ? ? ? ?使用G1收集器時,Java堆的內存布局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新牛代和 老年代不再是物理隔離的了,它們都是一部分Region(不需要連續(xù))的集合。
?????????G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java 堆中進行全區(qū)域的垃圾收集。 G1 跟蹤各個 Region 里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需時間的經驗值), 在后臺維護一個優(yōu)先列表, 每次根據允許的收集時間, 優(yōu)先回收價值最大的 Region (這也就是 Garbage-First 名稱的來由)。 這種使用Region 劃分內存空間以及有優(yōu)先級的區(qū)域回收方式, 保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
????????在G1 收集器中, Region 之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用, 虛擬機都是使用Remembered Set 來避免全堆掃描的。G1 中每個Region 都有一個與之對應的Remembered Set, 虛擬機發(fā)現程序在對Reference 類型的數據進行寫操作時, 會產生一個Write Barrier 暫時中斷寫操作, 檢查Reference 引用的對象是否處于不同的Region 之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象), 如果是, 便通過CardTable 把相關引用信息記錄到被引用對象所屬的Region 的Remembered Set 之中。當進行內存回收時, 在GC 根節(jié)點的枚舉范圍中加入Remembered Set 即可保證不對全堆掃描也不會有遺漏。
如果不計算維護Remembered Set 的操作, GI 收槳器的運作大致可劃分為以下兒個步驟:
1初始標記 (Initial Marking)
2并發(fā)標記 (Concurrent Marking)
3最終標記 (Final Marking)
4篩選回收 (Live Data Counting and Evacuation)
前面幾個于CMS類似,最終標記階需要把Remembered Set Logs 的數據合并到Remembered Set中,需要停頓線程,但是可并行執(zhí)行。最后在篩選回收階段對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。

垃圾收集器參數總結
