深刻理解JVM,7種垃圾收集器

本人免費整理了Java高級資料,一共30G,須要本身領取。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q
html

 

若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。Java虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣的廠商、版本的虛擬機所提供的垃圾收集器均可能會有很大差異,而且通常都會提供參數供用戶根據本身的應用特色和要求組合出各個年代所使用的收集器。接下來討論的收集器基於JDK1.7 Update 14 以後的HotSpot虛擬機(在此版本中正式提供了商用的G1收集器,以前G1仍處於實驗狀態),該虛擬機包含的全部收集器以下圖所示:面試

上圖展現了7種做用於不一樣分代的收集器,若是兩個收集器之間存在連線,就說明它們能夠搭配使用。虛擬機所處的區域,則表示它是屬於新生代收集器仍是老年代收集器。Hotspot實現瞭如此多的收集器,正是由於目前並沒有完美的收集器出現,只是選擇對具體應用最適合的收集器。算法

相關概念

並行和併發

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

吞吐量(Throughput)

吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即多線程

吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。併發

假設虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。佈局

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動做,由於Java對象大多都具有朝生夕滅的特性,因此Minor GC很是頻繁,通常回收速度也比較快。具體原理見上一篇文章。
  • 老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。Major GC的速度通常會比Minor GC慢10倍以上。

新生代收集器

Serial收集器

Serial(串行)收集器是最基本、發展歷史最悠久的收集器,它是採用複製算法的新生代收集器,曾經(JDK 1.3.1以前)是虛擬機新生代收集的惟一選擇。它是一個單線程收集器,只會使用一個CPU或一條收集線程去完成垃圾收集工做,更重要的是它在進行垃圾收集時,必須暫停其餘全部的工做線程,直至Serial收集器收集結束爲止(「Stop The World」)。這項工做是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的狀況下把用戶正常工做的線程所有停掉,這對不少應用來講是難以接收的。post

下圖展現了Serial 收集器(老年代採用Serial Old收集器)的運行過程:性能

爲了消除或減小工做線程因內存回收而致使的停頓,HotSpot虛擬機開發團隊在JDK 1.3以後的Java發展歷程中研發出了各類其餘的優秀收集器,這些將在稍後介紹。可是這些收集器的誕生並不意味着Serial收集器已經「老而無用」,實際上到如今爲止,它依然是HotSpot虛擬機運行在Client模式下的默認的新生代收集器。它也有着優於其餘收集器的地方:簡單而高效(與其餘收集器的單線程相比),對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到更高的單線程收集效率。網站

在用戶的桌面應用場景中,分配給虛擬機管理的內存通常不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的內存,桌面應用基本不會再大了),停頓時間徹底能夠控制在幾十毫秒最多一百毫秒之內,只要不頻繁發生,這點停頓時間能夠接收。因此,Serial收集器對於運行在Client模式下的虛擬機來講是一個很好的選擇。url

ParNew 收集器

ParNew收集器就是Serial收集器的多線程版本,它也是一個新生代收集器。除了使用多線程進行垃圾收集外,其他行爲包括Serial收集器可用的全部控制參數、收集算法(複製算法)、Stop The World、對象分配規則、回收策略等與Serial收集器徹底相同,二者共用了至關多的代碼。

ParNew收集器的工做過程以下圖(老年代採用Serial Old收集器):

ParNew收集器除了使用多線程收集外,其餘與Serial收集器相比並沒有太多創新之處,但它倒是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關的重要緣由是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工做,CMS收集器是JDK 1.5推出的一個具備劃時代意義的收集器,具體內容將在稍後進行介紹。

ParNew 收集器在單CPU的環境中絕對不會有比Serial收集器有更好的效果,甚至因爲存在線程交互的開銷,該收集器在經過超線程技術實現的兩個CPU的環境中都不能百分之百地保證能夠超越。在多CPU環境下,隨着CPU的數量增長,它對於GC時系統資源的有效利用是頗有好處的。它默認開啓的收集線程數與CPU的數量相同,在CPU很是多的狀況下可以使用-XX:ParallerGCThreads參數設置。

Parallel Scavenge 收集器

Parallel Scavenge收集器也是一個並行的多線程新生代收集器,它也使用複製算法。Parallel Scavenge收集器的特色是它的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標是達到一個可控制的吞吐量(Throughput)。

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

Parallel Scavenge收集器除了會顯而易見地提供能夠精確控制吞吐量的參數,還提供了一個參數-XX:+UseAdaptiveSizePolicy,這是一個開關參數,打開參數後,就不須要手工指定新生代的大小(-Xmn)、Eden和Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種方式稱爲GC自適應的調節策略(GC Ergonomics)。自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。

另外值得注意的一點是,Parallel Scavenge收集器沒法與CMS收集器配合使用,因此在JDK 1.6推出Parallel Old以前,若是新生代選擇Parallel Scavenge收集器,老年代只有Serial Old收集器能與之配合使用。

老年代收集器

Serial Old收集器

Serial Old 是 Serial收集器的老年代版本,它一樣是一個單線程收集器,使用「標記-整理」(Mark-Compact)算法。

此收集器的主要意義也是在於給Client模式下的虛擬機使用。若是在Server模式下,它還有兩大用途:

  • 在JDK1.5 以及以前版本(Parallel Old誕生之前)中與Parallel Scavenge收集器搭配使用。
  • 做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

它的工做流程與Serial收集器相同,這裏再次給出Serial/Serial Old配合使用的工做流程圖:

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。前面已經提到過,這個收集器是在JDK 1.6中才開始提供的,在此以前,若是新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old之外別無選擇,因此在Parallel Old誕生之後,「吞吐量優先」收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工做流程與Parallel Scavenge相同,這裏給出Parallel Scavenge/Parallel Old收集器配合使用的流程圖:

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,它很是符合那些集中在互聯網站或者B/S系統的服務端上的Java應用,這些應用都很是重視服務的響應速度。從名字上(「Mark Sweep」)就能夠看出它是基於「標記-清除」算法實現的。

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是一款優秀的收集器,它的主要優勢在名字上已經體現出來了:併發收集、低停頓,所以CMS收集器也被稱爲併發低停頓收集器(Concurrent Low Pause Collector)。

缺點

  • 對CPU資源很是敏感 其實,面向併發設計的程序都對CPU資源比較敏感。在併發階段,它雖然不會致使用戶線程停頓,但會由於佔用了一部分線程(或者說CPU資源)而致使應用程序變慢,總吞吐量會下降。CMS默認啓動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程很多於25%的CPU資源,而且隨着CPU數量的增長而降低。可是當CPU不足4個時(好比2個),CMS對用戶程序的影響就可能變得很大,若是原本CPU負載就比較大,還要分出一半的運算能力去執行收集器線程,就可能致使用戶程序的執行速度突然下降了50%,其實也讓人沒法接受。
  • 沒法處理浮動垃圾(Floating Garbage) 可能出現「Concurrent Mode Failure」失敗而致使另外一次Full GC的產生。因爲CMS併發清理階段用戶線程還在運行着,伴隨程序運行天然就還會有新的垃圾不斷產生。這一部分垃圾出如今標記過程以後,CMS沒法再當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就被稱爲「浮動垃圾」。也是因爲在垃圾收集階段用戶線程還須要運行,那也就還須要預留有足夠的內存空間給用戶線程使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。
  • 標記-清除算法致使的空間碎片 CMS是一款基於「標記-清除」算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,每每出現老年代空間剩餘,但沒法找到足夠大連續空間來分配當前對象。

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的舊對象來獲取更好的收集效果。
  • 空間整合 G1從總體來看是基於「標記-整理」算法實現的收集器,從局部(兩個Region之間)上來看是基於「複製」算法實現的。這意味着G1運行期間不會產生內存空間碎片,收集後能提供規整的可用內存。此特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發下一次GC。
  • 可預測的停頓 這是G1相對CMS的一大優點,下降停頓時間是G1和CMS共同的關注點,但G1除了下降停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在GC上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。

橫跨整個堆內存

在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老生代,而G1再也不是這樣。G1在使用時,Java堆的內存佈局與其餘收集器有很大區別,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,而都是一部分Region(不須要連續)的集合。

創建可預測的時間模型

G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內能夠獲取儘量高的收集效率。

避免全堆掃描——Remembered Set

G1把Java堆分爲多個Region,就是「化整爲零」。可是Region不多是孤立的,一個對象分配在某個Region中,能夠與整個Java堆任意的對象發生引用關係。在作可達性分析肯定對象是否存活的時候,須要掃描整個Java堆才能保證準確性,這顯然是對GC效率的極大傷害。

爲了不全堆掃描的發生,虛擬機爲G1中每一個Region維護了一個與之對應的Remembered Set。虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。


若是不計算維護Remembered Set的操做,G1收集器的運做大體可劃分爲如下幾個步驟:

  • 初始標記(Initial Marking) 僅僅只是標記一下GC Roots 能直接關聯到的對象,而且修改TAMS(Nest Top Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確能夠的Region中建立對象,此階段須要停頓線程,但耗時很短。
  • 併發標記(Concurrent Marking) 從GC Root 開始對堆中對象進行可達性分析,找到存活對象,此階段耗時較長,但可與用戶程序併發執行。
  • 最終標記(Final Marking) 爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的Remembered Set Logs裏面,最終標記階段須要把Remembered Set Logs的數據合併到Remembered Set中,這階段須要停頓線程,可是可並行執行。
  • 篩選回收(Live Data Counting and Evacuation) 首先對各個Region中的回收價值和成本進行排序,根據用戶所指望的GC 停頓是時間來制定回收計劃。此階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅度提升收集效率。

經過下圖能夠比較清楚地看到G1收集器的運做步驟中併發和須要停頓的階段(Safepoint處):

總結

 

------------------------推薦閱讀------------------------

2019年JVM最新面試題,必須收藏它

最全面的阿里多線程面試題,你能回答幾個?

Java面試題:Java中的集合及其繼承關係

花了近十年的時間,整理出史上最全面Java面試題

相關文章
相關標籤/搜索