在Java垃圾回收機制中說到Java垃圾回收中涉及到的收集算法,若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。這裏討論的收集器基於JDK 1.7 Update 14以後的 HotSpot 虛擬機,這個虛擬機包含的全部收集器以下圖所示:算法
上圖展現了7種做用於不一樣分代的收集器,若是兩個收集器之間存在連線,就說明它們能夠搭配使用。虛擬機所處的區域,則表示它是屬於新生代收集器仍是老年代收集器。接下來將逐一介紹這些收集器的特性、基本原理和使用場景,並重點分析CMS和G1這兩款相對複雜的收集器,瞭解它們的部分運做細節。編程
Serial收集器是最基本、發展歷史最悠久的收集器,曾經是虛擬機新生代收集的惟一選擇。這是一個單線程的收集器,但它的「單線程」的意義並不只僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工做,更重要的是在它進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束。"Stop The World"這個名字也許聽起來很酷,但這項工做其實是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的狀況下把用戶正常工做的線程所有停掉,這對不少應用來講都是難以接受的。下圖示意了Serial/Serial Old收集器的運行過程:服務器
實際上到如今爲止,它依然是虛擬機運行在Client模式下的默認新生代收集器。它也有着優於其餘收集器的地方:簡單而高效(與其餘收集器的單線程比),對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到最高的單線程收集效率。微信
在用戶的桌面應用場景中,分配給虛擬機管理的內存通常來講不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的內存,桌面應用基本上不會再大了),停頓時間徹底能夠控制在幾十毫秒最多一百多毫秒之內,只要不是頻繁發生,這點停頓是能夠接受的。因此,Serial收集器對於運行在Client模式下的虛擬機來講是一個很好的選擇。markdown
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集以外,其他行爲包括Serial收集器可用的全部控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器徹底同樣,在實現上,這兩種收集器也共用了至關多的代碼。ParNew收集器的工做過程以下圖所示:數據結構
ParNew收集器除了多線程收集以外,其餘與Serial收集器相比並無太多創新之處,但它倒是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的緣由是,除了Serial收集器外,目前只有它能與CMS收集器(併發收集器,後面有介紹)配合工做。多線程
ParNew收集器在單CPU的環境中不會有比Serial收集器更好的效果,甚至因爲存在線程交互的開銷,該收集器在經過超線程技術實現的兩個CPU的環境中都不能百分之百地保證能夠超越Serial收集器。固然,隨着可使用的CPU的數量的增長,它對於GC時系統資源的有效利用仍是頗有好處的。它默認開啓的收集線程數與CPU的數量相同,在CPU很是多(如32個)的環境下,可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。併發
注意,從ParNew收集器開始,後面還會接觸到幾款併發和並行的收集器。這裏有必要先解釋兩個名詞:併發和並行。這兩個名詞都是併發編程中的概念,在談論垃圾收集器的上下文語境中,它們能夠解釋以下:佈局
並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。post
併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上。
Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器……看上去和ParNew都同樣,那它有什麼特別之處呢?
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)。
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)收集器外別無選擇(Parallel Scavenge收集器沒法與CMS收集器配合工做)。
因爲老年代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個步驟,包括:
初始標記(CMS initial mark)
併發標記(CMS concurrent mark)
從新標記(CMS remark)
併發清除(CMS concurrent sweep)
其中,初始標記、從新標記這兩個步驟仍然須要"Stop The World"。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC RootsTracing的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。CMS收集器的工做過程以下圖所示:
CMS是一款優秀的收集器,它的主要優勢在名字上已經體現出來了:併發收集、低停頓,可是CMS還遠達不到完美的程度,它有如下3個明顯的缺點:
致使吞吐量下降。CMS收集器對CPU資源很是敏感。其實,面向併發設計的程序都對CPU資源比較敏感。在併發階段,它雖然不會致使用戶線程停頓,可是會由於佔用了一部分線程(或者說CPU資源)而致使應用程序變慢,總吞吐量會下降。CMS默認啓動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程很多於25%的CPU資源,而且隨着CPU數量的增長而降低。可是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大,若是原本CPU負載就比較大,還分出一半的運算能力去執行收集器線程,就可能致使用戶程序的執行速度突然下降了50%,其實也讓人沒法接受。
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"失敗,性能反而下降。
產生空間碎片。 CMS是一款基於「標記—清除」算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,每每會出現老年代還有很大空間剩餘,可是沒法找到足夠大的連續空間來分配當前對象,不得不提早觸發一次Full GC 。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認就是開啓的),用於在CMS收集器頂不住要進行Full GC時開啓內存碎片的合併整理過程,內存整理的過程是沒法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機設計者還提供了另一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC 後,跟着來一次帶壓縮的(默認值爲0,表示每次進入Full GC時都進行碎片整理)。
G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,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再也不是這樣。使用G1收集器時,Java堆的內存佈局就與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。內存結構以下圖:
這裏有三個關於內存的概念:代,區和內存分段。
G1把堆內存分爲年輕代和老年代。年輕代分爲Eden和Survivor兩個區,老年代分爲Old和Humongous兩個區。代和區都是邏輯概念。G1把堆內存分爲大小相等的內存分段,默認狀況下會把內存分爲2048個內存分段,能夠用-XX:G1HeapRegionSize調整內存分段的個數。好比32G堆內存,2048個內存分段每段的大小爲16M。這至關於把內存化整爲零。內存分段是物理概念,表明實際的物理內存空間。每一個內存分段均可以被標記爲Eden區,Survivor區,Old區,或者Humongous區。這樣屬於不一樣代,不一樣區的內存分段就能夠沒必要是連續內存空間了。
新分配的對象會被分配到Eden區的內存分段上,每一次年輕代的回收過程都會把Eden區存活的對象複製到Survivor區的內存分段上,把Survivor區繼續存活的對象年齡加1,若是Survivor區的存活對象年齡達到某個閾值(好比15,能夠設置),Survivor區的對象會被複制到Old區。複製過程是把源內存分段中全部存活的對象複製到空的目標內存分段上,複製完成後,源內存分段沒有了存活對象,變成了可使用的空的Eden內存分段了;而目標內存分段的對象都是連續存儲的,沒有碎片,因此複製過程能夠達到內存整理的效果,減小碎片。Humongous區用於保存大對象,若是一個對象佔用的空間超過內存分段的一半(好比上面的8M),則此對象將會被分配在Humongous區。若是對象的大小超過一個甚至幾個分段的大小,則對象會分配在物理連續的多個Humongous分段上。Humongous對象由於佔用內存較大而且連續會被優先回收。
G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由),保證了G1收集器在有限的時間內能夠獲取儘量高的收集效率。
理解回收過程,須要先了解記憶集合(Remembered Set),如下簡稱RS。爲了在回收單個內存分段的時候沒必要對整個堆內存的對象進行掃描(單個內存分段中的對象可能被其餘內存分段中的對象引用)引入了RS數據結構。RS使得G1能夠在年輕代回收的時候沒必要去掃描老年代的對象,從而提升了性能。每個內存分段都對應一個RS,RS保存了來自其餘分段內的對象對於此分段的引用。對於屬於年輕代的內存分段(Eden和Survivor區的內存分段)來講,RS只保存來自老年代的對象的引用。這是由於年輕代回收是針對所有年輕代的對象的,反正全部年輕代內部的對象引用關係都會被掃描,因此RS不須要保存來自年輕代內部的引用。對於屬於老年代分段的RS來講,也只會保存來自老年代的引用,這是由於老年代的回收以前會先進行年輕代的回收,年輕代回收後Eden區變空了,G1會在老年代回收過程當中掃描Survivor區到老年代的引用。
RS裏的引用信息是怎麼樣填充和維護的呢?簡而言之就是JVM會對應用程序的每個引用賦值語句object.field=object進行記錄和處理,把引用關係更新到RS中。可是這個RS的更新並非實時的。G1維護了一個Dirty Card Queue。對於應用程序的引用賦值語句object.field=object,JVM會在以前和以後執行特殊的操做以在dirty card queue中入隊一個保存了對象引用信息的card。在年輕代回收的時候,G1會對Dirty Card Queue中全部的card進行處理,以更新RS,保證RS實時準確的反映引用關係。那爲何不在引用賦值語句處直接更新RS呢?這是爲了性能的須要,RS的處理須要線程同步,開銷會很大,使用隊列性能會好不少。
在G1收集器中,Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。
若是不計算維護Remembered Set的操做,G1收集器的運做大體可劃分爲如下幾個步驟:
初始標記(Initial Marking)
併發標記(Concurrent Marking)
最終標記(Final Marking)
篩選回收(Live Data Counting and Evacuation)
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收集器的運做步驟中併發和須要停頓的階段:
JVM啓動時,G1先準備好Eden區,程序在運行過程當中不斷建立對象到Eden區,當全部的Eden區都滿了,G1會啓動一次年輕代垃圾回收過程。年輕代只會回收Eden區和Survivor區。首先G1中止應用程序的執行(Stop-The-World),G1建立回收集(Collection Set),回收集是指須要被回收的內存分段的集合,年輕代回收過程的回收集包含年輕代Eden區和Survivor區全部的內存分段。而後開始以下回收過程:
第一階段,掃描根。
根是指static變量指向的對象,正在執行的方法調用鏈條上的局部變量等。根引用連同RS記錄的外部引用做爲掃描存活對象的入口。
第二階段,更新RS。
處理dirty card queue中的card,更新RS。此階段完成後,RS能夠準確的反映老年代對所在的內存分段中對象的引用。
第三階段,處理RS。
識別被老年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認爲是存活的對象。
第四階段,複製對象。
此階段,對象樹被遍歷,Eden區內存段中存活的對象會被複制到Survivor區中空的內存分段,Survivor區內存段中存活的對象若是年齡未達閾值,年齡會加1,達到閥值會被會被複制到Old區中空的內存分段。
第五階段,處理引用。
處理Soft,Weak,Phantom,Final,JNI Weak 等引用。
當整個堆內存(包括老年代和新生代)被佔滿必定大小的時候(默認是45%,能夠經過-XX:InitiatingHeapOccupancyPercent進行設置),老年代回收過程會被啓動。具體檢測堆內存使用狀況的時機是年輕代回收以後或者houmongous對象分配以後。老年代回收包含標記老年代內的對象是否存活的過程,標記過程是和應用程序併發運行的(不須要Stop-The-World)。
應用程序會改變指針的指向,併發執行的標記過程怎麼能保證標記過程沒有問題呢?併發標記過程有一種情形會對存活的對象標記不到。假設有對象A,B和C,一開始的時候B.c=C,A.c=null。當A的對象樹先被掃描標記,接下來開始掃描B對象樹,此時標記線程被應用程序線程搶佔後停下來,應用程序把A.c=C,B.c=null。當標記線程恢復執行的時候C對象已經標記不到了,這時候C對象實際是存活的,這種情形被稱做對象丟失。G1解決的方法是在對象引用被設置爲空的語句(好比B.c=null)時,把原先指向的對象(C對象)保存到一個隊列,表明它多是存活的。而後會有一個從新標記(Remark)過程處理這些對象,從新標記過程是Stop-The-World的,因此能夠保證標記的正確性。上述這種標記方法被稱爲開始時快照技術(SATB,Snapshot At The Begging)。這種方式會形成某些是垃圾的對象也被當作是存活的,因此G1會使得佔用的內存被實際須要的內存大。
具體標記過程以下:
先進行一次年輕代回收過程,這個過程是Stop-The-World的。老年代的回收基於年輕代的回收(好比須要年輕代回收過程對於根對象的收集,初始的存活對象的標記)。
恢復應用程序線程的執行。
開始老年代對象的標記過程。此過程是與應用程序線程併發執行的。標記過程會記錄弱引用狀況,還會計算出每一個分段的對象存活數據(好比分段內存活對象所佔的百分比)。
Stop-The-World。
從新標記(Remark)。此階段從新標記前面提到的STAB隊列中的對象(例子中的C對象),還會處理弱引用。
回收百分之百爲垃圾的內存分段。注意:不是百分之百爲垃圾的內存分段並不會被處理,這些內存分段中的垃圾是在混合回收過程(Mixed GC)中被回收的。因爲Humongous對象會獨佔整個內存分段,若是Humongous對象變爲垃圾,則內存分段百分百爲垃圾,因此會在第一時間被回收掉。
恢復應用程序線程的執行。
併發標記過程結束之後,緊跟着就會開始混合回收過程。混合回收的意思是年輕代和老年代會同時被回收。併發標記結束之後,老年代中百分百爲垃圾的內存分段被回收了,部分爲垃圾的內存分段被計算了出來。默認狀況下,這些老年代的內存分段會分8次(能夠經過-XX:G1MixedGCCountTarget設置)被回收。混合回收的回收集(Collection Set)包括八分之一的老年代內存分段,Eden區內存分段,Survivor區內存分段。混合回收的算法和年輕代回收的算法徹底同樣,只是回收集多了老年代的內存分段。具體過程請參考上面的年輕代回收過程。
因爲老年代中的內存分段默認分8次回收,G1會優先回收垃圾多的內存分段。垃圾佔內存分段比例越高的,越會被先回收。而且有一個閾值會決定內存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默認爲65%,意思是垃圾佔內存分段比例要達到65%纔會被回收。若是垃圾佔比過低,意味着存活的對象佔比高,在複製的時候會花費更多的時間。
混合回收並不必定要進行8次。有一個閾值-XX:G1HeapWastePercent,默認值爲10%,意思是容許整個堆內存中有10%的空間被浪費,意味着若是發現能夠回收的垃圾佔堆內存的比例低於10%,則再也不進行混合回收。由於GC會花費不少的時間可是回收到的內存卻不多。
Full GC是指上述方式不能正常工做,G1會中止應用程序的執行(Stop-The-World),使用單線程的內存回收算法進行垃圾回收,性能會很是差,應用程序停頓時間會很長。要避免Full GC的發生,一旦發生須要進行調整。何時回發生Full GC呢?好比堆內存過小,當G1在複製存活對象的時候沒有空的內存分段可用,則會回退到full gc,這種狀況能夠經過增大內存解決。
1.線程本地分配緩衝區(TLAB: Thread Local Allocation Buffer)
因爲堆內存是應用程序共享的,應用程序的多個線程在分配內存的時候須要加鎖以進行同步。爲了不加鎖,提升性能每個應用程序的線程會被分配一個TLAB。TLAB中的內存來自於G1年輕代中的內存分段。當對象不是Humongous對象,TLAB也能裝的下的時候,對象會被優先分配於建立此對象的線程的TLAB中。這樣分配會很快,由於TLAB隸屬於線程,因此不須要加鎖。
2.GC「提高」線程本地分配緩衝區(PLAB: Promotion Thread Local Allocation Buffer)
前面提到過,G1會在年輕代回收過程當中把Eden區中的對象複製(「提高」)到Survivor區中,Survivor區中的對象複製到Old區中。G1的回收過程是多線程執行的,爲了不多個線程往同一個內存分段進行復制,那麼複製的過程也須要加鎖。爲了不加鎖,G1的每一個線程都關聯了一個PLAB,這樣就不須要進行加鎖了。
3.Remembered Set粒度
其實RS的存儲分三種粒度,前面提到的Card是最小的一種粒度。粒度的存在是由於某些內存分段中的對象可能很熱門,被來自很是多的區的對象所引用,爲了不保存太多的數據,會以更大的粒度來保存這些引用,好比最大的粒度是用一個bitmap來保存其餘內存分段對RS所對應的內存分段的引用。每個內存分段對應一個bit,若是bit爲0表示該bit對應的內存分段中沒有引用,爲1表示有引用。這種方式會減小RS的數據,可是會增長掃描和標記時的開銷,由於須要掃描全部bit爲1的內存分段中的對象以肯定具體是來自哪一個對象的引用。
本文摘自《深刻理解Java虛擬機》