1. 程式人生 > >JVM學習(4) 垃圾收集器

JVM學習(4) 垃圾收集器

如果說收集演算法是記憶體回收的方法論,垃圾收集器就是記憶體回收的具體實現。Java虛擬機器規範中對垃圾收集器應該如何實現並沒有任何規定,因此不同的廠商、不同版本的虛擬機器所提供的垃圾收集器都可能會有很大的差別,並且一般都會提供引數供使用者根據自己的應用特點和要求組合出各個年代所使用的收集器。這裡討論的收集器基於Sun HotSpot虛擬機器1.6版Update 22,這個虛擬機器包含的所有收集器如圖所示:

在這裡插入圖片描述

圖中展示了7種作用於不同分代的收集器(包括JDK 1.6_Update14後引入的Early Access版G1收集器),如果兩個收集器之間存在連線,就說明它們可以搭配使用。

Serial收集器

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

在這裡插入圖片描述

寫道這裡,筆者似乎已經把Serial收集器描述成一個老而無用,食之無味棄之可惜的雞肋了,但實際上到現在為止,它依然是虛擬機器執行在Client模式下的預設新生代收集器。它也有著優於其他收集器的地方:簡單而高效(與其他收集器的單執行緒比),對於限定單個CPU的環境來收,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。

ParNew收集器

ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多執行緒進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制引數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一樣,實際上這兩種收集器也共用了相當多的程式碼。ParNewS收集器的工作過程如圖:

在這裡插入圖片描述

ParNew收集器除了多執行緒收集之外,其他與Serial收集器相比並沒有太多創新之處,但它卻是許多執行在Server模式下的虛擬機器中首選的新生代收集器,其中有一個與效能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。

不幸的是,它作為老年代的收集器,卻無法與JDK1.4.0種已經存在新生代收集器Parallel Scavenge配合工作,所以在JDK1.5種使用CMS來收集老年代的時候,新生代只能選擇ParNew或Serial收集器中的一個。

ParNew收集器在單CPU的環境中絕對不會有Serial收集器更好的效果,甚至由於存線上程互動的開銷,該收集器在通過超執行緒技術實現的兩個CPU的環境中都不能百分之百地保證能超越Serial收集器。

注意 從ParNew收集器開始,後面還將會接觸到幾款併發和並行地收集器。有必要先解釋兩個名詞:併發和並行。

  • 並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態。
  • 併發(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行),使用者程式繼續執行,而垃圾收集程式運行於另一個CPU上。

Parallel Scavenge收集器

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

Parallel Scavenge收集器的特點是它關注點與其他收集器不同,CMS等收集器的關注點儘可能地縮短垃圾收集時使用者執行緒地停頓時間,而Parallel Scavenge收集器地目標則是達到一個可控制的 吞吐量(Throughput)。所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值。

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

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

MaxGCPauseMillis引數允許的值是一個大於0的毫秒數,收集器將盡力保證記憶體回收花費的時間不超過設定值。

GCTimeRation引數的值應當是一個大於0小於100的證書,也就是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。

由於與吞吐量關係密切,Parallel Scavenge收集器也經常被稱為“吞吐量優先”收集器。除了上述兩個引數之外,Parallel Scavenge收集器還有一個引數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關引數,當這個引數開啟之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對像年齡(-XX:PretenureSizeThreshold)等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最適合的停頓時間或最大吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics)。如果讀者對於收集器運作原理不太瞭解,手工優化存在困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把記憶體管理的調優任務交給虛擬機器去完成將是一個很不錯的選擇。只需要把基本的記憶體資料設定好(如-Xmx設定最大堆),然後使用MaxGCPauseMillis引數(更關注最大停頓時間)或GCTimeRatio引數(更關注吞吐量)給虛擬機器設立一個優化目標,那具體細節引數的調節工作就由虛擬機器完成了。自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用“標記-整理”演算法。這個收集器的主要意義也是被Client模式下的虛擬機器使用。如果在Server模式下,它主要還有兩大用途:一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個就是作為CMS收集器的後被預案,在併發收集發生Concurrent Mode Failure的時候使用。

Paralel Old收集器

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

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

CMS收集器

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

它的運作過程相當於前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:

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

其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記以下GC Roots能直接關聯到的物件,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始化標記階段稍長一些,但遠比並發標記的時間短。

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

在這裡插入圖片描述

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

  • CMS收集器對CPU資源非常敏感。
  • CMS收集器無法處理浮動垃圾(Floating Garbage),
  • 收集結束時會產生大量空間碎片

G1收集器

G1(Garbage First)收集器是當前收集器技術發展的最前沿成果,在JDK1.6_Update14中提供了Early Access版本的G1收集器以供試用。這裡只對G1收集器進行簡單的介紹。

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

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