垃圾收集器,詳解jdk參數配置

Java虛擬機規範中對垃圾收集器應該如何實現並沒有任何規定,因此,不同廠商,不同版本的虛擬機所提供的垃圾收集器都可能會有很大的差別,並且一般都會提供參數供用戶根據自己的應用特點和要求組合出各個年代所使用的收集器。這裏討論的收集器基於Sun HotSpot虛擬機1.6版。

如果兩個收集器之間存在連線,就說明它們可以搭配使用。

在介紹這些收集器各自的特性之前,我們先來明確一點:雖然我們是在對各個收集器進行比較,但並非爲了挑選一個最好的收集器出來。因爲知道現在位置還沒有最好的收集器出現,更加沒有萬能的收集器,所有我們選擇的只是對具體應用最適合的收集器。這點不需要多加解釋就能證明:如果有一種放之四海皆準,任何場景下都使用的完美收集器存在,那麼HotSpot虛擬機就沒有必要實現那麼多不同的收集器了。

1.Serial收集器

Serial收集器是最基本,歷史最悠久的收集器,曾經(在JDK1.3.1之前)是新生代收集的唯一選擇。這個收集器是一個單線程的收集器,但它的“單線程”的意義並不僅僅是說明它只會用一個CPU或者一條收集線程去完成垃圾收集工作,更重要的是它進行垃圾收集的時候,必須暫停其他所有的工作線程(Sun 將這件事情稱爲“Stop The World”),直到它收集結束。“stop The World”這個名字也許聽起來很酷,但這項工作實際上是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的情況下把用戶的正常工作的線程全部停掉,這對很多應用來說都是難以接受的。你想想,要是你的電腦每運行一個小時就會暫停響應5分鐘,你會有什麼樣的心情?

圖3-6示意了Serial/Serial Old收集器的運行過程。

 

                                   圖3——6

對於“Stop The World”帶給用戶的惡劣體驗,虛擬機的設計者們表示完全理解,但也表示非常委屈:“你媽媽在給你打掃房間的時候,肯定也會讓你老老實實地在椅子上或房間外待着,如果她一邊打掃,你一邊亂扔紙屑,這房間還能打掃完嗎?”這確實是一個合情合理的矛盾,雖然垃圾收集這項工作聽起來和打掃房間屬於一個性質的,但實際上肯定還要比打掃房間複雜得多啊!

從JDK1.3開始,一直到現在,HotSpot虛擬機開發團隊爲消除或減少工作線程因內存回收而導致停頓的努力一直在進行着,從Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS),Garbage First(G1)收集器,我們看到了一個個越來越優秀(也越來越複雜)的收集器的出現,用戶線程的停頓時間在不斷縮短,但是仍然沒有辦法完全消除(這裏暫不包括RTSJ中的收集器)。尋找更優秀的垃圾收集器的工作仍在繼續!

寫到這裏,看似Serial收集器描述成一個老而無用,食之無味棄之可惜的雞肋了,但實際到現在爲止,它依然是虛擬機運行在Client模式下的默認新生代收集器。它有着優於其他收集器的地方:簡單而高效(與其他收集器的單線程比),對於限定單個CPU的環境來說,Serial收集器由於線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。在用戶的桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的內存,桌面應用不會再大了),停頓時間完全可以控制在幾十毫秒以內,只要不是頻繁發生,這點停頓是可以,這點停頓是可以接受的。所以,Serial收集器對於運行在Client模式下的虛擬機來說是一個很好的選擇。

2.ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了採用多線程進行垃圾收集外,其餘行爲包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio -XX:Pretenure-SizeThreshold -XX:HandlePromotionFailure等),收集算法,Stop The World,對象分配規則,回收策略等都與Serial收集器完全一樣,實現上這兩種收集器也共用了相當多的代碼。ParNew收集器的工作過程如圖3-7所示:


                                                                             圖3-7

ParNew收集器除了多線程收集之外,其他與Serial收集器相比並沒有太多創新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。在JDK1.5時期,HotSpot推出了一款在強交互應用中幾乎可稱爲有劃時代意義的垃圾收集器——CMS收集器(Concurent Mark Sweep,本節稍後將詳細介紹這款收集器),這款收集器是HotSpot虛擬機中第一款真正意義上的併發(Concurrent)收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作,用前面的那個例子的話來說,就是做到了在你媽媽打掃房間的時候你還能同時往地上扔紙屑。

不幸的是,它作爲老年代的收集器,卻無法與JDK1.4.0中已經存在的新生代收集器ParallelScavenge 配合工作,所以在JDK1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或Serial收集器中的一個。ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。

ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU環境中都不能百分之百地保證能超越Serial收集器。當然隨着可以使用的CPU的數量的增加,它對於GC時系統資源的利用還是很有好處的。它默認開啓的收集線程數與CPU的數量相同,在CPU非常多(譬如32個,現在CPU動輒就4核加超線程,服務器超過32個邏輯CPU的情況越來越多了)的環境下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

注意:從ParNew收集器開始,後面還將會接觸到幾款併發和並行的收集器。在大家可能產生疑惑之前,有必要先解釋兩個名詞,併發和並行。這兩個名詞都是併發變成中的概念,在談論垃圾收集器的上下文語境中,它們可以解釋爲:

並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。

併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序繼續運行,而垃圾收集程序運行於另個CPU上。

3.Parallel Scavenge收集器

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

Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點與其他收集器不同,CMS等收集器的關注點儘可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶的時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

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

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

MaxGCPauseMills參數允許的值是一個大於0的毫秒數,收集器將盡力保證內存回收花費的時間不超過設定值。不過大家不要異想天開的認爲如果把這個參數的值設置的少校一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300M新生代肯定比手機500M快些吧,這也直接導致垃圾收集發生得更頻繁一些,原來10秒收集一次,每次停頓100毫秒,現在變成5秒收集一次,每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。

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

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

如果讀者對於收集器運作原理不太瞭解,手工優化存在困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成將是一個很不錯的選擇。只需要把基本的內存數據設置好(如-Xmx設置最大堆),然後使用MaxGCPauseMills參數(更關注最大停頓時間)或GCTimeRatio參數(更關注吞吐量)給虛擬機設立一個優化目標,那具體細節參數的調節工作就由虛擬機完成了。自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。

4.Serial Old收集器


Serial Old是Serial收集器的老年代版本,它同樣是一個 單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是被Client模式下的虛擬機使用。如果在Server模式下,它主要還有兩大用途:一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure的時候使用。這兩點將在後面的內容中詳細講解。Serial Old收集器的goon工作過程如圖3-8所示。

                                    圖3——8

5.Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在JDK1.6中才開始提供的,在此之前,新生代的Parallel Scavenge 收集器一直處於比較尷尬的狀態。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了SerialOld(PS MarkSweep)收集器外別無選擇(還記得上面說過Paralel Scavenge收集器無法與CMS收集器配合工作嗎?)。由於單線程的老年代Serial Old收集器在服務端應用性能上的”拖累“,即便是用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,又因爲老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。

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


                                圖3——9

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用都集中在互聯網或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基於”標記-清除“算法的,它的運作過程相對於前面集中收集器來說要更復雜一些,整個過程分爲4個步驟,包括:

  • 初始標記(CMS initial mark)
  • 併發標記(CMS concurent mark)
  • 重新標記(CMS remark)
  • 併發清除(CMS concurrent sweep)

其中初始標記,重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots Tracing的過程,而重新標記階段則是爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標價階段稍長一些,但遠比並發標記時間短。

由於整個過程中耗時最長的併發標記和併發清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。通過圖3——10可以比較清除的看到CMS收集器的運作步驟中併發需要停頓的時間。

CMS是一款優秀的收集器,他的最主要優點在名字上已經體現出來了:併發收集,低停頓,Sun的一些官方文檔裏面也稱之爲併發低停頓收集器(Concurrent  Low Pause Collector)。但是CMS還遠達不到完美的程度,他有以下三個下顯著的缺點:

  • CMS收集器對CPU資源非常敏感。其實,面向併發設計的程序都對CPU資源比較敏感。在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS默認啓動的回收線程數是(CPU + 3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程最多佔用哦個不超過25%的CPU資源。但是當CPU不足4個時(譬如兩個),那麼CMS對用戶程序的影響可能變得很大,如果CPU負載本來就較大的時候還分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了50%,這也很讓人受不了。
  • CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一個Full GC的產生。魚魚CMS併發清理階段用戶線程還在運行着,伴隨程序的運行自然還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在本次收集中處理掉它們,只好留待下一次GC時再將其清理掉。這一部分垃圾就稱爲“浮動垃圾”。也是由於在垃圾收集階段用戶線程還需要運行,即還需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程序運作使用。在默認設置下,CMS收集器在老年代使用了68%的空間後就會被激活,這是一個偏保守的設置,如果在應用中老年代增長不是太快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高出發百分比,以便降低內存回收次數以獲取更好的性能。要是CMS運行期間預留的內存無法滿足程序需要就會出現一次“Conturrent Mode Failure”失敗,這時候虛擬機將啓動後備預案:臨時啓動Serial Old收集器來重新進行老年代的收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiationgOccupancyFraction設置得太高將會很容易導致大量“Concurrent Mode Failure”失敗,性能反而降低。
  • 還有最後一個缺點,在本節開頭說過,CMS是一款基於“標記-清除”算法實現的收集器,如果讀者對前面這種算法介紹還有印象的話,就可能想到這意味着收集結束時會產生大量空間碎片。空間碎片過多時,將會給大對象分配帶來很大的麻煩,往往會出現老年代還有很大的空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一個Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UserCMSCompactAtFullCoolection開關參數,用於在“享受”完Full GC服務之後額外免費附送一個碎片整理過程,內存整理的過程是無法併發的。空間碎片問題沒有了,但停頓時間不得不變長了。虛擬機設計者們還提供了另外一個參數:-XX:CMSFullGCBeforeCompaction,這個參數用於設置多少次不壓縮的Full GC後,跟着來一次帶壓縮的。


7.G1收集器

G1(Garbage First)收集器是當前收集器技術發展的最前沿成果,在JDK1.6中提供了Early Acess版本的G1收集器以供試用。

G1收集器垃圾收集器理論進一步發展的產物,它與前面的CMS收集器相比有兩個顯著的改進:一是G1收集器是基於“標記-整理”算法實現的收集器,也就是說它不會產生空間碎片,這對於長時間運行的應用系統來說非常重要。二是它可以非常精確地控制停頓,既能讓使用者明確指定在一個長度爲M毫秒的時間片內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。

G1收集器可以實現在基本不犧牲吞吐量的前提下完成低停頓的內存回收,這是由於它能夠極力地避免全區域的垃圾收集,之前的收集器進行收集的範圍都是整個新生代或老年代,而G!將整個java堆(包括新生代,老年代)劃分爲多個大小固定的獨立區域(Region),並且跟蹤這些區域裏面的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域(這就是Garbage First)的由來。區域劃分及有優先級的區域回收,保證了G1收集器在優先的時間內可以獲得最高的收集效率。

垃圾收集器參數總結

JDK1.6中的各種垃圾收集器到此已經全部介紹完畢,在描述過程中提到了很多虛擬機飛穩定的運行參數。下表整理了以供參考:



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章