【深入Java虛擬機器】之六:GC收集器以及JDK7,JDK8中JVM記憶體變化
Java與C++之間有一堵由記憶體動態分配和垃圾收集技術所圍成的“高牆”,牆外面的人想進去,牆裡面的人卻想出來。
GC收集器
如果說收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現。
Java虛擬機器規範中對垃圾收集器應該如何實現並沒有任何規定,因此不同的廠商、不同版本的虛擬機器所提供的垃圾收集器都可能會有很大差別,並且一般都會提供引數供使用者根據自己的應用特點和要求組合出各個年代所使用的收集器。下圖中展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。虛擬機器所處的區域,則表示它是屬於新生代收集器還是老年代收集器。
一、Serial收集器
Serial收集器是最基本、發展歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是虛擬機器新生代收集的唯一選擇。
特性:
這個收集器是一個單執行緒的收集器,但它的“單執行緒”的意義並不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。Stop The World
優勢:
簡單而高效(與其他收集器的單執行緒比),對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。
二、ParNew收集器
特性:
ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多條執行緒進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制引數、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一樣,在實現上,這兩種收集器也共用了相當多的程式碼。
優勢:
除了多執行緒收集以外,跟Serial收集器一樣,很重要的原因是:除了Serial收集器外,目前只有它能與CMS收集器配合工作。CMS作為老年代的收集器,卻無法與JDK 1.4.0中已經存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。
Serial收集器 VS ParNew收集器:
ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於存線上程互動的開銷,該收集器在通過超執行緒技術實現的兩個CPU的環境中都不能百分之百地保證可以超越Serial收集器。
然而,隨著可以使用的CPU的數量的增加,它對於GC時系統資源的有效利用還是很有好處的。
三、Parallel Scavenge收集器
特性:
Parallel Scavenge收集器是一個新生代收集器,它也是使用複製演算法的收集器,又是並行的多執行緒收集器。Parallel Scavenge收集器的目標是達到一個可控的吞吐量,可以高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。
Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器與ParNew收集器的一個重要區別是它具有自適應調節策略。Parallel Scavenge收集器有一個引數-XX:+UseAdaptiveSizePolicy。當這個引數開啟之後,就不需要手工指定新生代的大小、Eden與Survivor區的比例、晉升老年代物件年齡等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics)。
四、Serial Old收集器
特性:
Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用標記-整理演算法。它主要有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。
五、Parallel Old收集器
特性:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。
優勢:
在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。這個收集器是在JDK 1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器外別無選擇。由於老年代Serial Old收集器在服務端應用效能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,由於單執行緒的老年代收集中無法充分利用伺服器多CPU的處理能力,在老年代很大而且硬體比較高階的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。直到Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的應用組合。
六、CMS收集器
特性:
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
CMS收集器是基於“標記—清除”演算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分為4個步驟:
初始標記(CMS initial mark) 初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快,需要“Stop The World”。
併發標記(CMS concurrent mark) 併發標記階段就是進行GC Roots Tracing的過程。
重新標記(CMS remark) 重新標記階段是為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,仍然需要“Stop
The World”。
併發清除(CMS concurrent sweep) 併發清除階段會清除物件。
由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以,從總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。
優點:
CMS是一款優秀的收集器,它的主要優點在名字上已經體現出來了:併發收集、低停頓。
缺點:
CMS收集器對CPU資源非常敏感
其實,面向併發設計的程式都對CPU資源比較敏感。在併發階段,它雖然不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒(或者說CPU資源)而導致應用程式變慢,總吞吐量會降低。
CMS預設啟動的回收執行緒數是(CPU數量+3)/ 4,也就是當CPU在4個以上時,併發回收時垃圾收集執行緒不少於25%的CPU資源,並且隨著CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對使用者程式的影響就可能變得很大。
CMS收集器無法處理浮動垃圾
CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。
由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。
也是由於在垃圾收集階段使用者執行緒還需要執行,那也就還需要預留有足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程式運作使用。要是CMS執行期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將啟動後備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。
CMS收集器會產生大量空間碎片
CMS是一款基於“標記—清除”演算法實現的收集器,這意味著收集結束時會有大量空間碎片產生。
空間碎片過多時,將會給大物件分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。
七、G1收集器
特性:
G1(Garbage-First)是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是未來可以替換掉JDK 1.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毫秒。
在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。
執行過程:
G1收集器的運作大致可劃分為以下幾個步驟:
初始標記(Initial Marking)
初始標記階段僅僅只是標記一下GC Roots能直接關聯到的物件,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式併發執行時,能在正確可用的Region中建立新物件,這階段需要停頓執行緒,但耗時很短。
併發標記(Concurrent Marking)
併發標記階段是從GCRoot開始對堆中物件進行可達性分析,找出存活的物件,這階段耗時較長,但可與使用者程式併發執行。
最終標記(Final Marking)
最終標記階段是為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程Remembered Set Logs裡面,最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set中,這階段需要停頓執行緒,但是可並行執行。
篩選回收(Live Data Counting and Evacuation)
篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅提高收集效率。
總結
關於GC的種類歸納,推薦檢視HotSpot VM GC 的種類 這篇部落格,有細緻的分類和總結。
JDK7,JDK8中JVM記憶體變化
Java7中已經將執行時常量池從永久代移除,在Java 堆(Heap)中開闢了一塊區域存放執行時常量池。
Java8中,已經徹底沒有了永久代,將方法區直接放在一個與堆不相連的本地記憶體區域,這個區域被叫做元空間。
JDK1.7之前的版本
其中最上一層是Nursery記憶體,一個物件被建立以後首先被放到Nursery中的Eden內
存中,如果存活期超兩個Survivor之後就會被轉移到長時記憶體中(Old Generation)中。
JDK1.8版本
JDK8中把存放元資料中的永久記憶體從堆記憶體中移到了本地記憶體(native memory)中,這樣永久記憶體就不再佔用堆記憶體,它可以通過自動增長來避免JDK7以及前期版本中常見的永久記憶體錯誤(Java.lang.OutOfMemoryError: PermGen)。
JDK8也提供了一個新的設定Matespace記憶體大小的引數:-XX:MaxMetaspaceSize=128m
注意:如果不設定JVM將會根據一定的策略自動增加本地元記憶體空間。如果你設定的元記憶體空間過小,你的應用程式可能得到以下錯誤:java.lang.OutOfMemoryError: Metadata space
《深入理解Java虛擬機器:JVM高階特性與最佳實戰》 周志明 著
HotSpot VM GC 的種類
深入理解JVM(5) : Java垃圾收集器
JVM7、8詳解及優化