垃圾收集器是內存回收的具體實現。如下討論的收集器是基於JDK1.7Update14以後的HotSpot虛擬機。這個虛擬機包含的全部收集器有:算法
上圖展現了7種做用於不一樣分代的收集器,若是兩個收集器之間存在連線,就說明它們能夠搭配使用。虛擬機所處的區域,則表示它是屬於新生代收集器仍是老年代收集器。接下來筆者將逐一介紹這些收集器的特性、基本原理和使用場景,並重點分析CMS和G1這兩款相對複雜的收集器,瞭解它們的部分運做細節。服務器
直到如今爲止尚未最好的收集器出現,更加沒有萬能的收集器,因此咱們選擇的只是對具體應用最合適的收集器。若是有一種放之四海、任何場景下都適用的完美收集器存在,那HotSpot虛擬機就不必實現那麼多不一樣的收集器了。多線程
Serial收集器是最基本、發展歷史最悠久的收集器,在JDK1.3.1以前是虛擬機新生代收集的惟一選擇。併發
是單線程的收集器,進行垃圾收集時,必須 暫停其餘全部的工做線程,直到它收集結束。缺點:用戶停頓很長佈局
運行過程:性能
新生代採用 複製算法優化
從JDK1.3開始,HotSpot虛擬機開發團隊爲消除或者減小工做線程因內存回收而致使停頓的努力一直在進行着,從Serial收集器到Parallel收集器,網站
再到Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成果Garbage First(G1)收集器,用戶停頓在不斷縮短。操作系統
可是依然是虛擬機運行在Client模式下的默認新生代收集器。線程
優勢:簡單而高效,對於限定單個CPU的環境來講,Serial 收集器因爲沒有現成交互的開銷,專心作垃圾收集天然得到最高的單線程收集效率。
在用戶桌面應用場景中,分配給虛擬機管理的內存通常來講不會很大,收集幾十M甚至一兩百M的新生代,停頓時間徹底能夠控制在幾十毫秒最多一百多毫秒之內,只要不頻繁發生,這點停頓是能夠接受的。
因此Serial收集器對於運行在Client模式下的虛擬機來講是一個很好的選擇。
ParNew是Serial的多線程版本。複製算法
除了使用多條線程進行垃圾收集以外,其他行爲包括Serial收集器可用的全部控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器徹底同樣,在實現上,這兩種收集器也共用了至關多的代碼。
ParNew收集器的工做過程:
它倒是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的緣由是,除了Serial收集器外,目前只有它能與CMS收集器配合工做。
ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也可使用-XX:+UseParNewGC選項來強制指定它。
ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至因爲存在線程交互的開銷,固然,隨着可使用的CPU的數量的增長,它對於GC時系統資源的有效利用仍是頗有好處的。它默認開啓的收集線程數與CPU的數量相同,在CPU很是多(譬如32個,如今CPU動輒就4核加超線程,服務器超過32個邏輯CPU的狀況愈來愈多了)的環境下,可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
並行 Parallel:指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。
併發 Concurrent:指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上。
Parallel-Scavenge是一個新生代收集器,它也是使用複製算法的收集器,又是並行的 多線程收集器。
Parallel Scavenge收集器的特色是它的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是 達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。
工做過程:
Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。
MaxGCPauseMillis參數容許的值是一個大於0的毫秒數,收集器將盡量地保證內存回收花費的時間不超過設定值。不過你們不要認爲若是把這個參數的值設置得稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代確定比收集500MB快吧,這也直接致使垃圾收集發生得更頻繁一些,原來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設置最大堆),而後使用MaxGCPauseMillis參數(更關注最大停頓時間)或GCTimeRatio(更關注吞吐量)參數給虛擬機設立一個優化目標,那具體細節參數的調節工做就由虛擬機完成了。自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。
Serial Old是Serial收集器的老年代版本,它一樣是一個 單線程 收集器,使用「標記-整理」算法。
這個收集器的主要意義也是在於給Client模式下的虛擬機使用。
若是在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另外一種用途就是做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。這兩點都將在後面的內容中詳細講解。
Serial Old收集器的工做過程
Parallel Old是Parallel Scavenge收集器的老年代版本,使用 多線程 和「標記-整理」算法。
這個收集器是在JDK 1.6中才開始提供的,在此以前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。緣由是,若是新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇,因爲老年代Serial Old收集器在服務端應用性能上的「拖累」,使用了Parallel Scavenge收集器也未必能在總體應用上得到吞吐量最大化的效果,因爲單線程的老年代收集中沒法充分利用服務器多CPU的處理能力,在老年代很大並且硬件比較高級的環境中,這種組合的吞吐量甚至還不必定有ParNew加CMS的組合「給力」。
直到Parallel Old收集器出現後,「吞吐量優先」收集器終於有了比較 名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器。
Parallel Old收集器的工做過程:
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就很是符合這類應用的需求。
從名字(包含「Mark Sweep」)上就能夠看出,CMS收集器是基於「標記—清除」算法實現的,它的運做過程相對於前面幾種收集器來講更復雜一些,整個過程分爲4個步驟,包括:
1. 初始標記(CMS initial mark)
2. 併發標記(CMS concurrent mark)
3. 從新標記(CMS remark)
4. 併發清除(CMS concurrent sweep)
其中,初始標記、從新標記 這兩個步驟仍然須要「Stop The World」。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC RootsTracing的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的 併發標記和併發清除 過程收集器線程均可以 與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。
CMS收集器的工做過程:
CMS是一款優秀的收集器,它的主要優勢在名字上已經體現出來了:併發收集、低停頓,Sun公司的一些官方文檔中也稱之爲併發低停頓收集器(Concurrent Low Pause Collector)。可是CMS還遠達不到完美的程度,它有如下3個明顯的缺點:
1. CMS收集器對CPU資源很是敏感。其實,面向併發設計的程序都對CPU資源比較敏感。在併發階段,它雖然不會致使用戶線程停頓,可是會由於佔用了一部分線程(或者說CPU資源)而致使應用程序變慢,總吞吐量會下降。CMS默認啓動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程很多於25%的CPU資源,而且隨着CPU數量的增長而降低。可是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大,若是原本CPU負載就比較大,還分出一半的運算能力去執行收集器線程,就可能致使用戶程序的執行速度突然下降了50%,其實也讓人沒法接受。爲了應付這種狀況,虛擬機提供了一種稱爲「增量式併發收集器」(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器變種,所作的事情和單CPU年代PC機操做系統使用搶佔式來模擬多任務機制的思想同樣,就是在併發標記、清理的時候讓GC線程、用戶線程交替運行,儘可能減小GC線程的獨佔資源的時間,這樣整個垃圾收集的過程會更長,但對用戶程序的影響就會顯得少一些,也就是速度降低沒有那麼明顯。實踐證實,增量時的CMS收集器效果很通常,在目前版本中,i-CMS已經被聲明爲「deprecated」,即再也不提倡用戶使用。
2. CMS收集器沒法處理浮動垃圾(Floating Garbage),可能出現「Concurrent Mode Failure」失敗而致使另外一次Full GC的產生。因爲CMS併發清理階段用戶線程還在運行着,伴隨程序運行天然就還會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,CMS沒法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲「浮動垃圾」。也是因爲在垃圾收集階段用戶線程還須要運行,那也就還須要預留有足夠的內存空間給用戶線程使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。在JDK 1.5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活,這是一個偏保守的設置,若是在應用中老年代增加不是太快,能夠適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提升觸發百分比,以便下降內存回收次數從而獲取更好的性能,在JDK 1.6中,CMS收集器的啓動閾值已經提高至92%。要是CMS運行期間預留的內存沒法知足程序須要,就會出現一次「Concurrent Mode Failure」失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。因此說參數-XX:CMSInitiatingOccupancyFraction設置得過高很容易致使大量「Concurrent Mode Failure」失敗,性能反而下降。
3. 還有最後一個缺點,在本節開頭說過,CMS是一款基於「標記—清除」算法實現的收集器,若是讀者對前面這種算法介紹還有印象的話,就可能想到這意味着收集結束時 會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,每每會出現老年代還有很大空間剩餘,可是沒法找到足夠大的連續空間來分配當前對象,不得不提早觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認就是開啓的),用於在CMS收集器頂不住要進行FullGC時開啓內存碎片的合併整理過程,內存整理的過程是沒法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機設計者還提供了另一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,表示每次進入Full GC時都進行碎片整理)。
G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,早在JDK 1.7剛剛確立項目目標,Sun公司給出的JDK 1.7 RoadMap裏面,它就被視爲JDK 1.7中HotSpot虛擬機的一個重要進化特徵。從JDK 6u14中開始就有Early Access版本的G1收集器供開發人員實驗、試用,由此開始G1收集器的「Experimental」狀態持續了數年時間,直至JDK 7u4,Sun公司才認爲它達到足夠成熟的商用程度,移除了「Experimental」的標識。
G1是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是(在比較長期的)將來能夠替換掉JDK 1.5中發佈的CMS收集器。與其餘GC收集器相比,G1具有以下特色。
並行與併發:G1能充分利用多CPU、多核環境下的硬件優點,使用多個CPU(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毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。
在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局就與其餘收集器有很大差異,它將整個Java堆劃分爲 多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。
G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大Region(這也就是Garbage-First名稱的來由)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在 有限的時間內能夠獲取儘量高的收集效率。
G1把內存「化整爲零」的思路,理解起來彷佛很容易,但其中的實現細節卻遠遠沒有想象中那樣簡單,不然也不會從2004年Sun實驗室發表第一篇G1的論文開始直到今天(將近10年時間)纔開發出G1的商用版。筆者以一個細節爲例:把Java堆分爲多個Region後,垃圾收集是否就真的能以Region爲單位進行了?聽起來瓜熟蒂落,再仔細想一想就很容易發現問題所在:Region不多是孤立的。一個對象分配在某個Region中,它並不是只能被本Region中的其餘對象引用,而是能夠與整個Java堆任意的對象發生引用關係。那在作可達性斷定肯定對象是否存活的時候,豈不是還得掃描整個Java堆才能保證準確性?這個問題其實並不是在G1中才有,只是在G1中更加突出而已。在之前的分代收集中,新生代的規模通常都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的對象時也面臨相同的問題,若是回收新生代時也不得不一樣時掃描老年代的話,那麼Minor GC的效率可能降低很多。
在G1收集器中,Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用 Remembered Set 來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。
若是不計算維護Remembered Set的操做,G1收集器的運做大體可劃分爲如下幾個步驟:
1. 初始標記(Initial Marking)
2. 併發標記(Concurrent Marking)
3. 最終標記(Final Marking)
4. 篩選回收(Live Data Counting and Evacuation)
對CMS收集器運做過程熟悉的讀者,必定已經發現G1的前幾個步驟的運做過程和CMS有不少類似之處。初始標記階段僅僅只是標記一下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立新對象,這階段須要停頓線程,但耗時很短。併發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。而最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線Remembered Set Logs裏面,最終標記階段須要把Remembered Set Logs的數據合併到Remembered Set中,這階段須要停頓線程,可是可並行執行。最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃,從Sun公司透露出來的信息來看,這個階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅提升收集效率。
G1收集器的運做步驟中併發和須要停頓的階段。