詳解 JVM Garbage First(G1) 垃圾收集器(轉載)

前言 
Garbage First(G1)是垃圾收集領域的最新成果,同時也是HotSpot在JVM上力推的垃圾收集器,並賦予取代CMS的使命。若是使用Java 8/9,那麼有很大可能但願對G1收集器進行評估。本文詳細首先對JVM其餘的垃圾收集器進行總結,並與G1進行了簡單的對比;而後經過G1的內存模型、G1的活動週期,對G1的工做機制進行了介紹;同時還在介紹過程當中,描述了可能須要引發注意的優化點。筆者但願經過本文,讓有必定JVM基礎的讀者能儘快掌握G1的知識點。算法

第一章 概述

G1(Garbage First)垃圾收集器是當今垃圾回收技術最前沿的成果之一。早在JDK7就已加入JVM的收集器你們庭中,成爲HotSpot重點發展的垃圾回收技術。同優秀的CMS垃圾回收器同樣,G1也是關注最小時延的垃圾回收器,也一樣適合大尺寸堆內存的垃圾收集,官方也推薦使用G1來代替選擇CMS。G1最大的特色是引入分區的思路,弱化了分代的概念,合理利用垃圾收集各個週期的資源,解決了其餘收集器甚至CMS的衆多缺陷。安全

第二章 JVM GC收集器的回顧與比較

JVM GC收集器成員

從JDK3(1.3)開始,HotSpot團隊一直努力朝着高效收集、減小停頓(STW: Stop The World)的方向努力,也貢獻了從串行到CMS乃至最新的G1在內的一系列優秀的垃圾收集器。上圖展現了JDK的垃圾回收你們庭,以及相互之間的組合關係,下面就幾種典型的組合應用進行簡單的介紹。服務器

串行收集器

串行收集器組合

串行收集器組合 Serial + Serial Old數據結構

開啓選項:-XX:+SerialGC多線程

串行收集器是最基本、發展時間最長、久經考驗的垃圾收集器,也是client模式下的默認收集器配置。併發

串行收集器採用單線程stop-the-world的方式進行收集。當內存不足時,串行GC設置停頓標識,待全部線程都進入安全點(Safepoint)時,應用線程暫停,串行GC開始工做,採用單線程方式回收空間並整理內存。單線程也意味着複雜度更低、佔用內存更少,但同時也意味着不能有效利用多核優點。事實上,串行收集器特別適合堆內存不高、單核甚至雙核CPU的場合。負載均衡

並行收集器

並行收集器組合

並行收集器組合 Parallel Scavenge + Parallel Oldjvm

開啓選項:-XX:+UseParallelGC-XX:+UseParallelOldGC(可互相激活)性能

並行收集器是以關注吞吐量爲目標的垃圾收集器,也是server模式下的默認收集器配置,對吞吐量的關注主要體如今年輕代Parallel Scavenge收集器上。優化

並行收集器與串行收集器工做模式類似,都是stop-the-world方式,只是暫停時並行地進行垃圾收集。年輕代採用複製算法,老年代採用標記-整理,在回收的同時還會對內存進行壓縮。關注吞吐量主要指年輕代的Parallel Scavenge收集器,經過兩個目標參數-XX:MaxGCPauseMills-XX:GCTimeRatio,調整新生代空間大小,來下降GC觸發的頻率。並行收集器適合對吞吐量要求遠遠高於延遲要求的場景,而且在知足最差延時的狀況下,並行收集器將提供最佳的吞吐量。

併發標記清除收集器

併發標記清除收集器組合

併發標記清除收集器組合 ParNew + CMS + Serial Old

開啓選項:-XX:+UseConcMarkSweepGC

併發標記清除(CMS)是以關注延遲爲目標、十分優秀的垃圾回收算法,開啓後,年輕代使用STW式的並行收集,老年代回收採用CMS進行垃圾回收,對延遲的關注也主要體如今老年代CMS上。

年輕代ParNew與並行收集器相似,而老年代CMS每一個收集週期都要經歷:初始標記、併發標記、從新標記、併發清除。其中,初始標記以STW的方式標記全部的根對象;併發標記則同應用線程一塊兒並行,標記出根對象的可達路徑;在進行垃圾回收前,CMS再以一個STW進行從新標記,標記那些由mutator線程(指引發數據變化的線程,即應用線程)修改而可能錯過的可達對象;最後獲得的不可達對象將在併發清除階段進行回收。值得注意的是,初始標記和從新標記都已優化爲多線程執行。CMS很是適合堆內存大、CPU核數多的服務器端應用,也是G1出現以前大型應用的首選收集器。

可是CMS並不完美,它有如下缺點:

  1. 因爲併發進行,CMS在收集與應用線程會同時會增長對堆內存的佔用,也就是說,CMS必需要在老年代堆內存用盡以前完成垃圾回收,不然CMS回收失敗時,將觸發擔保機制,串行老年代收集器將會以STW的方式進行一次GC,從而形成較大停頓時間;
  2. 標記清除算法沒法整理空間碎片,老年代空間會隨着應用時長被逐步耗盡,最後將不得不經過擔保機制對堆內存進行壓縮。CMS也提供了參數-XX:CMSFullGCsBeForeCompaction(默認0,即每次都進行內存整理)來指定多少次CMS收集以後,進行一次壓縮的Full GC。

Garbage First

G1收集器

Garbage First (G1)

開啓選項:-XX:+UseG1GC

以前介紹的幾組垃圾收集器組合,都有幾個共同點:

  1. 年輕代、老年代是獨立且連續的內存塊;
  2. 年輕代收集使用單eden、雙survivor進行復制算法;
  3. 老年代收集必須掃描整個老年代區域;
  4. 都是以儘量少而塊地執行GC爲設計原則。

G1垃圾收集器也是以關注延遲爲目標、服務器端應用的垃圾收集器,被HotSpot團隊寄予取代CMS的使命,也是一個很是具備調優潛力的垃圾收集器。雖然G1也有相似CMS的收集動做:初始標記、併發標記、從新標記、清除、轉移回收,而且也以一個串行收集器作擔保機制,但單純地以相似前三種的過程描述顯得並非很穩當。事實上,G1收集與以上三組收集器有很大不一樣:

  1. G1的設計原則是」首先收集儘量多的垃圾(Garbage First)「。所以,G1並不會等內存耗盡(串行、並行)或者快耗盡(CMS)的時候開始垃圾收集,而是在內部採用了啓發式算法,在老年代找出具備高收集收益的分區進行收集。同時G1能夠根據用戶設置的暫停時間目標自動調全年輕代和總堆大小,暫停目標越短年輕代空間越小、總空間就越大;
  2. G1採用內存分區(Region)的思路,將內存劃分爲一個個相等大小的內存分區,回收時則以分區爲單位進行回收,存活的對象複製到另外一個空閒分區中。因爲都是以相等大小的分區爲單位進行操做,所以G1自然就是一種壓縮方案(局部壓縮);
  3. G1雖然也是分代收集器,但整個內存分區不存在物理上的年輕代與老年代的區別,也不須要徹底獨立的survivor(to space)堆作複製準備。G1只有邏輯上的分代概念,或者說每一個分區均可能隨G1的運行在不一樣代之間先後切換;
  4. G1的收集都是STW的,但年輕代和老年代的收集界限比較模糊,採用了混合(mixed)收集的方式。即每次收集既可能只收集年輕代分區(年輕代收集),也可能在收集年輕代的同時,包含部分老年代分區(混合收集),這樣即便堆內存很大時,也能夠限制收集範圍,從而下降停頓。

第三章 G1的內存模型

分區概念

G1分區示意圖

分區

分區 Region

G1採用了分區(Region)的思路,將整個堆空間分紅若干個大小相等的內存區域,每次分配對象空間將逐段地使用內存。所以,在堆的使用上,G1並不要求對象的存儲必定是物理上連續的,只要邏輯上連續便可;每一個分區也不會肯定地爲某個代服務,能夠按需在年輕代和老年代之間切換。啓動時能夠經過參數-XX:G1HeapRegionSize=n可指定分區大小(1MB~32MB,且必須是2的冪),默認將整堆劃分爲2048個分區。

卡片

卡片 Card

在每一個分區內部又被分紅了若干個大小爲512 Byte卡片(Card),標識堆內存最小可用粒度全部分區的卡片將會記錄在全局卡片表(Global Card Table)中,分配的對象會佔用物理上連續的若干個卡片,當查找對分區內對象的引用時即可經過記錄卡片來查找該引用對象(見RSet)。每次對內存的回收,都是對指定分區的卡片進行處理。

堆 Heap

G1一樣能夠經過-Xms/-Xmx來指定堆空間大小。當發生年輕代收集或混合收集時,經過計算GC與應用的耗費時間比,自動調整堆空間大小。若是GC頻率過高,則經過增長堆尺寸,來減小GC頻率,相應地GC佔用的時間也隨之下降;目標參數-XX:GCTimeRatio即爲GC與應用的耗費時間比,G1默認爲9,而CMS默認爲99,由於CMS的設計原則是耗費在GC上的時間儘量的少。另外,當空間不足,如對象空間分配或轉移失敗時,G1會首先嚐試增長堆空間,若是擴容失敗,則發起擔保的Full GC。Full GC後,堆尺寸計算結果也會調整堆空間。

分代模型

G1的分代模型

分代

分代 Generation

分代垃圾收集能夠將關注點集中在最近被分配的對象上,而無需整堆掃描,避免長命對象的拷貝,同時獨立收集有助於下降響應時間。雖然分區使得內存分配再也不要求緊湊的內存空間,但G1依然使用了分代的思想。與其餘垃圾收集器相似,G1將內存在邏輯上劃分爲年輕代和老年代,其中年輕代又劃分爲Eden空間和Survivor空間。但年輕代空間並非固定不變的,當現有年輕代分區佔滿時,JVM會分配新的空閒分區加入到年輕代空間。

整個年輕代內存會在初始空間-XX:G1NewSizePercent(默認整堆5%)與最大空間-XX:G1MaxNewSizePercent(默認60%)之間動態變化,且由參數目標暫停時間-XX:MaxGCPauseMillis(默認200ms)、須要擴縮容的大小以及分區的已記憶集合(RSet)計算獲得。固然,G1依然能夠設置固定的年輕代大小(參數-XX:NewRatio-Xmn),但同時暫停目標將失去意義。

本地分配緩衝

本地分配緩衝 Local allocation buffer (Lab)

值得注意的是,因爲分區的思想,每一個線程都可以」認領」某個分區用於線程本地的內存分配,而不須要顧及分區是否連續。所以,每一個應用線程和GC線程都會獨立的使用分區,進而減小同步時間,提高GC效率,這個分區稱爲本地分配緩衝區(Lab)。

其中,應用線程能夠獨佔一個本地緩衝區(TLAB)來建立的對象,而大部分都會落入Eden區域(巨型對象或分配失敗除外),所以TLAB的分區屬於Eden空間;而每次垃圾收集時,每一個GC線程一樣能夠獨佔一個本地緩衝區(GCLAB)用來轉移對象,每次回收會將對象複製到Suvivor空間或老年代空間;對於從Eden/Survivor空間晉升(Promotion)到Survivor/老年代空間的對象,一樣有GC獨佔的本地緩衝區進行操做,該部分稱爲晉升本地緩衝區(PLAB)。

分區模型

G1的分區模型

G1對內存的使用以分區(Region)爲單位,而對對象的分配則以卡片(Card)爲單位。

巨型對象

巨型對象 Humongous Region

一個大小達到甚至超過度區大小一半的對象稱爲巨型對象(Humongous Object)。當線程爲巨型分配空間時,不能簡單在TLAB進行分配,由於巨型對象的移動成本很高,並且有可能一個分區不能容納巨型對象。所以,巨型對象會直接在老年代分配,所佔用的連續空間稱爲巨型分區(Humongous Region)。G1內部作了一個優化,一旦發現沒有引用指向巨型對象,則可直接在年輕代收集週期中被回收。

巨型對象會獨佔一個、或多個連續分區,其中第一個分區被標記爲開始巨型(StartsHumongous),相鄰連續分區被標記爲連續巨型(ContinuesHumongous)。因爲沒法享受Lab帶來的優化,而且肯定一片連續的內存空間須要掃描整堆,所以肯定巨型對象開始位置的成本很是高,若是能夠,應用程序應避免生成巨型對象。

已記憶集合

已記憶集合 Remember Set (RSet)

在串行和並行收集器中,GC經過整堆掃描,來肯定對象是否處於可達路徑中。然而G1爲了不STW式的整堆掃描,在每一個分區記錄了一個已記憶集合(RSet),內部相似一個反向指針,記錄引用分區內對象的卡片索引。當要回收該分區時,經過掃描分區的RSet,來肯定引用本分區內的對象是否存活,進而肯定本分區內的對象存活狀況。

事實上,並不是全部的引用都須要記錄在RSet中,若是一個分區肯定須要掃描,那麼無需RSet也能夠無遺漏的獲得引用關係。那麼引用源自本分區的對象,固然不用落入RSet中;同時,G1 GC每次都會對年輕代進行總體收集,所以引用源自年輕代的對象,也不須要在RSet中記錄。最後只有老年代的分區可能會有RSet記錄,這些分區稱爲擁有RSet分區(an RSet’s owning region)。

Per Region Table

Per Region Table (PRT)

RSet在內部使用Per Region Table(PRT)記錄分區的引用狀況。因爲RSet的記錄要佔用分區的空間,若是一個分區很是」受歡迎」,那麼RSet佔用的空間會上升,從而下降分區的可用空間。G1應對這個問題採用了改變RSet的密度的方式,在PRT中將會以三種模式記錄引用:

  • 稀少:直接記錄引用對象的卡片索引
  • 細粒度:記錄引用對象的分區索引
  • 粗粒度:只記錄引用狀況,每一個分區對應一個比特位

由上可知,粗粒度的PRT只是記錄了引用數量,須要經過整堆掃描才能找出全部引用,所以掃描速度也是最慢的。

收集集合 (CSet)

CSet收集示意圖

收集集合 CSet

收集集合(CSet)表明每次GC暫停時回收的一系列目標分區。在任意一次收集暫停中,CSet全部分區都會被釋放,內部存活的對象都會被轉移到分配的空閒分區中。所以不管是年輕代收集,仍是混合收集,工做的機制都是一致的。年輕代收集CSet只容納年輕代分區,而混合收集會經過啓發式算法,在老年代候選回收分區中,篩選出回收收益最高的分區添加到CSet中。

候選老年代分區的CSet准入條件,能夠經過活躍度閾值-XX:G1MixedGCLiveThresholdPercent(默認85%)進行設置,從而攔截那些回收開銷巨大的對象;同時,每次混合收集能夠包含候選老年代分區,可根據CSet對堆的總大小佔比-XX:G1OldCSetRegionThresholdPercent(默認10%)設置數量上限。

由上述可知,G1的收集都是根據CSet進行操做的,年輕代收集與混合收集沒有明顯的不一樣,最大的區別在於兩種收集的觸發條件。

年輕代收集集合

年輕代收集集合 CSet of Young Collection

應用線程不斷活動後,年輕代空間會被逐漸填滿。當JVM分配對象到Eden區域失敗(Eden區已滿)時,便會觸發一次STW式的年輕代收集。在年輕代收集中,Eden分區存活的對象將被拷貝到Survivor分區;原有Survivor分區存活的對象,將根據任期閾值(tenuring threshold)分別晉升到PLAB中,新的survivor分區和老年代分區。而原有的年輕代分區將被總體回收掉。

同時,年輕代收集還負責維護對象的年齡(存活次數),輔助判斷老化(tenuring)對象晉升的時候是到Survivor分區仍是到老年代分區。年輕代收集首先先將晉升對象尺寸總和、對象年齡信息維護到年齡表中,再根據年齡表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默認50%)、最大任期閾值-XX:MaxTenuringThreshold(默認15),計算出一個恰當的任期閾值,凡是超過任期閾值的對象都會被晉升到老年代。

混合收集集合

混合收集集合 CSet of Mixed Collection

年輕代收集不斷活動後,老年代的空間也會被逐漸填充。當老年代佔用空間超過整堆比IHOP閾值-XX:InitiatingHeapOccupancyPercent(默認45%)時,G1就會啓動一次混合垃圾收集週期。爲了知足暫停目標,G1可能不能一口氣將全部的候選分區收集掉,所以G1可能會產生連續屢次的混合收集與應用線程交替執行,每次STW的混合收集與年輕代收集過程相相似。

爲了肯定包含到年輕代收集集合CSet的老年代分區,JVM經過參數混合週期的最大總次數-XX:G1MixedGCCountTarget(默認8)、堆廢物百分比-XX:G1HeapWastePercent(默認5%)。經過候選老年代分區總數與混合週期最大總次數,肯定每次包含到CSet的最小分區數量;根據堆廢物百分比,當收集達到參數時,再也不啓動新的混合收集。而每次添加到CSet的分區,則經過計算獲得的GC效率進行安排。

第四章 G1的活動週期

G1垃圾收集活動彙總

G1垃圾收集活動週期圖

祭出一張總圖

RSet的維護

因爲不能整堆掃描,又須要計算分區確切的活躍度,所以,G1須要一個增量式的徹底標記併發算法,經過維護RSet,獲得準確的分區引用信息。在G1中,RSet的維護主要來源兩個方面:寫柵欄(Write Barrier)和併發優化線程(Concurrence Refinement Threads)

柵欄

柵欄代碼示意

柵欄 Barrier

咱們首先介紹一下柵欄(Barrier)的概念。柵欄是指在原生代碼片斷中,當某些語句被執行時,柵欄代碼也會被執行。而G1主要在賦值語句中,使用寫前柵欄(Pre-Write Barrrier)和寫後柵欄(Post-Write Barrrier)。事實上,寫柵欄的指令序列開銷很是昂貴,應用吞吐量也會根據柵欄複雜度而下降。

寫前柵欄 Pre-Write Barrrier

即將執行一段賦值語句時,等式左側對象將修改引用到另外一個對象,那麼等式左側對象原先引用的對象所在分區將所以喪失一個引用,那麼JVM就須要在賦值語句生效以前,記錄喪失引用的對象。JVM並不會當即維護RSet,而是經過批量處理,在未來RSet更新(見SATB)。

寫後柵欄 Post-Write Barrrier

當執行一段賦值語句後,等式右側對象獲取了左側對象的引用,那麼等式右側對象所在分區的RSet也應該獲得更新。一樣爲了下降開銷,寫後柵欄發生後,RSet也不會當即更新,一樣只是記錄這次更新日誌,在未來批量處理(見Concurrence Refinement Threads)。

起始快照算法

起始快照算法 Snapshot at the beginning (SATB)

Taiichi Tuasa貢獻的增量式徹底併發標記算法起始快照算法(SATB),主要針對標記-清除垃圾收集器的併發標記階段,很是適合G1的分區塊的堆結構,同時解決了CMS的主要煩惱:從新標記暫停時間長帶來的潛在風險。

SATB會建立一個對象圖,至關於堆的邏輯快照,從而確保併發標記階段全部的垃圾對象都能經過快照被鑑別出來。當賦值語句發生時,應用將會改變了它的對象圖,那麼JVM須要記錄被覆蓋的對象。所以寫前柵欄會在引用變動前,將值記錄在SATB日誌或緩衝區中。每一個線程都會獨佔一個SATB緩衝區,初始有256條記錄空間。當空間用盡時,線程會分配新的SATB緩衝區繼續使用,而原有的緩衝去則加入全局列表中。最終在併發標記階段,併發標記線程(Concurrent Marking Threads)在標記的同時,還會按期檢查和處理全局緩衝區列表的記錄,而後根據標記位圖分片的標記位,掃描引用字段來更新RSet。此過程又稱爲併發標記/SATB寫前柵欄。

併發優化線程

併發優化線程 Concurrence Refinement Threads

G1中使用基於Urs Hölzle的快速寫柵欄,將柵欄開銷縮減到2個額外的指令。柵欄將會更新一個card table type的結構來跟蹤代間引用。

當賦值語句發生後,寫後柵欄會先經過G1的過濾技術判斷是不是跨分區的引用更新,並將跨分區更新對象的卡片加入緩衝區序列,即更新日誌緩衝區或髒卡片隊列。與SATB相似,一旦日誌緩衝區用盡,則分配一個新的日誌緩衝區,並將原來的緩衝區加入全局列表中。

併發優化線程(Concurrence Refinement Threads),只專一掃描日誌緩衝區記錄的卡片來維護更新RSet,線程最大數目可經過-XX:G1ConcRefinementThreads(默認等於-XX:ParellelGCThreads)設置。併發優化線程永遠是活躍的,一旦發現全局列表有記錄存在,就開始併發處理。若是記錄增加很快或者來不及處理,那麼經過閾值-X:G1ConcRefinementGreenZone/-XX:G1ConcRefinementYellowZone/-XX:G1ConcRefinementRedZone,G1會用分層的方式調度,使更多的線程處理全局列表。若是併發優化線程也不能跟上緩衝區數量,則Mutator線程(Java應用線程)會掛起應用並被加進來幫助處理,直到所有處理完。所以,必須避免此類場景出現。

併發標記週期

併發標記週期 Concurrent Marking Cycle

併發標記週期是G1中很是重要的階段,這個階段將會爲混合收集週期識別垃圾最多的老年代分區。整個週期完成根標記、識別全部(可能)存活對象,並計算每一個分區的活躍度,從而肯定GC效率等級。

當達到IHOP閾值-XX:InitiatingHeapOccupancyPercent(老年代佔整堆比,默認45%)時,便會觸發併發標記週期。整個併發標記週期將由初始標記(Initial Mark)、根分區掃描(Root Region Scanning)、併發標記(Concurrent Marking)、從新標記(Remark)、清除(Cleanup)幾個階段組成。其中,初始標記(隨年輕代收集一塊兒活動)、從新標記、清除是STW的,而併發標記若是來不及標記存活對象,則可能在併發標記過程當中,G1又觸發了幾回年輕代收集。

併發標記線程

併發標記線程 Concurrent Marking Threads

併發標記位圖過程

要標記存活的對象,每一個分區都須要建立位圖(Bitmap)信息來存儲標記數據,來肯定標記週期內被分配的對象。G1採用了兩個位圖Previous Bitmap、Next Bitmap,來存儲標記數據,Previous位圖存儲上次的標記數據,Next位圖在標記週期內不斷變化更新,同時Previous位圖的標記數據也愈來愈過期,當標記週期結束後Next位圖便替換Previous位圖,成爲上次標記的位圖。同時,每一個分區經過頂部開始標記(TAMS),來記錄已標記過的內存範圍。一樣的,G1使用了兩個頂部開始標記Previous TAMS(PTAMS)、Next TAMS(NTAMS),記錄已標記的範圍。

在併發標記階段,G1會根據參數-XX:ConcGCThreads(默認GC線程數的1/4,即-XX:ParallelGCThreads/4),分配併發標記線程(Concurrent Marking Threads),進行標記活動。每一個併發線程一次只掃描一個分區,並經過」手指」指針的方式優化獲取分區。併發標記線程是爆發式的,在給定的時間段拼命幹活,而後休息一段時間,再拼命幹活。

每一個併發標記週期,在初始標記STW的最後,G1會分配一個空的Next位圖和一個指向分區頂部(Top)的NTAMS標記。Previous位圖記錄的上次標記數據,上次的標記位置,即PTAMS,在PTAMS與分區底部(Bottom)的範圍內,全部的存活對象都已被標記。那麼,在PTAMS與Top之間的對象都將是隱式存活(Implicitly Live)對象。在併發標記階段,Next位圖吸取了Previous位圖的標記數據,同時每一個分區都會有新的對象分配,則Top與NTAMS分離,前往更高的地址空間。在併發標記的一次標記中,併發標記線程將找出NTAMS與PTAMS之間的全部存活對象,將標記數據存儲在Next位圖中。同時,在NTAMS與Top之間的對象即成爲已標記對象。如此不斷地更新Next位圖信息,並在清除階段與Previous位圖交換角色。

初始標記

初始標記 Initial Mark

初始標記(Initial Mark)負責標記全部能被直接可達的根對象(原生棧對象、全局對象、JNI對象),根是對象圖的起點,所以初始標記須要將Mutator線程(Java應用線程)暫停掉,也就是須要一個STW的時間段。事實上,當達到IHOP閾值時,G1並不會當即發起併發標記週期,而是等待下一次年輕代收集,利用年輕代收集的STW時間段,完成初始標記,這種方式稱爲借道(Piggybacking)。在初始標記暫停中,分區的NTAMS都被設置到分區頂部Top,初始標記是併發執行,直到全部的分區處理完。

根分區掃描

根分區掃描 Root Region Scanning

在初始標記暫停結束後,年輕代收集也完成的對象複製到Survivor的工做,應用線程開始活躍起來。此時爲了保證標記算法的正確性,全部新複製到Survivor分區的對象,都須要被掃描並標記成根,這個過程稱爲根分區掃描(Root Region Scanning),同時掃描的Suvivor分區也被稱爲根分區(Root Region)。根分區掃描必須在下一次年輕代垃圾收集啓動前完成(併發標記的過程當中,可能會被若干次年輕代垃圾收集打斷),由於每次GC會產生新的存活對象集合。

併發標記

併發標記 Concurrent Marking

和應用線程併發執行,併發標記線程在併發標記階段啓動,由參數-XX:ConcGCThreads(默認GC線程數的1/4,即-XX:ParallelGCThreads/4)控制啓動數量,每一個線程每次只掃描一個分區,從而標記出存活對象圖。在這一階段會處理Previous/Next標記位圖,掃描標記對象的引用字段。同時,併發標記線程還會按期檢查和處理STAB全局緩衝區列表的記錄,更新對象引用信息。參數-XX:+ClassUnloadingWithConcurrentMark會開啓一個優化,若是一個類不可達(不是對象不可達),則在從新標記階段,這個類就會被直接卸載。全部的標記任務必須在堆滿前就完成掃描,若是併發標記耗時很長,那麼有可能在併發標記過程當中,又經歷了幾回年輕代收集。若是堆滿前沒有完成標記任務,則會觸發擔保機制,經歷一次長時間的串行Full GC。

存活數據計算

存活數據計算 Live Data Accounting

存活數據計算(Live Data Accounting)是標記操做的附加產物,只要一個對象被標記,同時會被計算字節數,並計入分區空間。只有NTAMS如下的對象會被標記和計算,在標記週期的最後,Next位圖將被清空,等待下次標記週期。

從新標記

從新標記 Remark

從新標記(Remark)是最後一個標記階段。在該階段中,G1須要一個暫停的時間,去處理剩下的SATB日誌緩衝區和全部更新,找出全部未被訪問的存活對象,同時安全完成存活數據計算。這個階段也是並行執行的,經過參數-XX:ParallelGCThread可設置GC暫停時可用的GC線程數。同時,引用處理也是從新標記階段的一部分,全部重度使用引用對象(弱引用、軟引用、虛引用、最終引用)的應用都會在引用處理上產生開銷。

清除

清除 Cleanup

緊挨着從新標記階段的清除(Clean)階段也是STW的。Previous/Next標記位圖、以及PTAMS/NTAMS,都會在清除階段交換角色。清除階段主要執行如下操做:

  1. RSet梳理,啓發式算法會根據活躍度和RSet尺寸對分區定義不一樣等級,同時RSet數理也有助於發現無用的引用。參數-XX:+PrintAdaptiveSizePolicy能夠開啓打印啓發式算法決策細節;
  2. 整理堆分區,爲混合收集週期識別回收收益高(基於釋放空間和暫停目標)的老年代分區集合;
  3. 識別全部空閒分區,即發現無存活對象的分區。該分區可在清除階段直接回收,無需等待下次收集週期。

年輕代收集/混合收集週期

年輕代收集和混合收集週期,是G1回收空間的主要活動。當應用運行開始時,堆內存可用空間還比較大,只會在年輕代滿時,觸發年輕代收集;隨着老年代內存增加,當到達IHOP閾值-XX:InitiatingHeapOccupancyPercent(老年代佔整堆比,默認45%)時,G1開始着手準備收集老年代空間。首先經歷併發標記週期,識別出高收益的老年代分區,前文已述。但隨後G1並不會立刻開始一次混合收集,而是讓應用線程先運行一段時間,等待觸發一次年輕代收集。在此次STW中,G1將保準整理混合收集週期。接着再次讓應用線程運行,當接下來的幾回年輕代收集時,將會有老年代分區加入到CSet中,即觸發混合收集,這些連續屢次的混合收集稱爲混合收集週期(Mixed Collection Cycle)。

GC工做線程數

GC工做線程數 -XX:ParallelGCThreads

JVM能夠經過參數-XX:ParallelGCThreads進行指定GC工做的線程數量。參數-XX:ParallelGCThreads默認值並非固定的,而是根據當前的CPU資源進行計算。若是用戶沒有指定,且CPU小於等於8,則默認與CPU核數相等;若CPU大於8,則默認JVM會通過計算獲得一個小於CPU核數的線程數;固然也能夠人工指定與CPU核數相等。

年輕代收集

年輕代收集 Young Collection

每次收集過程當中,既有並行執行的活動,也有串行執行的活動,但均可以是多線程的。在並行執行的任務中,若是某個任務太重,會致使其餘線程在等待某項任務的處理,須要對這些地方進行優化。

並行活動

外部根分區掃描 Ext Root Scanning:此活動對堆外的根(JVM系統目錄、VM數據結構、JNI線程句柄、硬件寄存器、全局變量、線程對棧根)進行掃描,發現那些沒有加入到暫停收集集合CSet中的對象。若是系統目錄(單根)擁有大量加載的類,最終可能其餘並行活動結束後,該活動依然沒有結束而帶來的等待時間。

更新已記憶集合 Update RS:併發優化線程會對髒卡片的分區進行掃描更新日誌緩衝區來更新RSet,但只會處理全局緩衝列表。做爲補充,全部被記錄可是尚未被優化線程處理的剩餘緩衝區,會在該階段處理,變成已處理緩衝區(Processed Buffers)。爲了限制花在更新RSet的時間,能夠設置暫停佔用百分比-XX:G1RSetUpdatingPauseTimePercent(默認10%,即-XX:MaxGCPauseMills/10)。值得注意的是,若是更新日誌緩衝區更新的任務不下降,單純地減小RSet的更新時間,會致使暫停中被處理的緩衝區減小,將日誌緩衝區更新工做推到併發優化線程上,從而增長對Java應用線程資源的爭奪。

RSet掃描 Scan RS:在收集當前CSet以前,考慮到分區外的引用,必須掃描CSet分區的RSet。若是RSet發生粗化,則會增長RSet的掃描時間。開啓診斷模式-XX:UnlockDiagnosticVMOptions後,經過參數-XX:+G1SummarizeRSetStats能夠肯定併發優化線程是否可以及時處理更新日誌緩衝區,並提供更多的信息,來幫助爲RSet粗化總數提供窗口。參數-XX:G1SummarizeRSetStatsPeriod=n可設置RSet的統計週期,即經歷多少此GC後進行一次統計

代碼根掃描 Code Root Scanning:對代碼根集合進行掃描,掃描JVM編譯後代碼Native Method的引用信息(nmethod掃描),進行RSet掃描。事實上,只有CSet分區中的RSet有強代碼根時,纔會作nmethod掃描,查找對CSet的引用。

轉移和回收 Object Copy:經過選定的CSet以及CSet分區完整的引用集,將執行暫停時間的主要部分:CSet分區存活對象的轉移、CSet分區空間的回收。經過工做竊取機制來負載均衡地選定複製對象的線程,而且複製和掃描對象被轉移的存活對象將拷貝到每一個GC線程分配緩衝區GCLAB。G1會經過計算,預測分區複製所花費的時間,從而調全年輕代的尺寸。

終止 Termination:完成上述任務後,若是任務隊列已空,則工做線程會發起終止要求。若是還有其餘線程繼續工做,空閒的線程會經過工做竊取機制嘗試幫助其餘線程處理。而單獨執行根分區掃描的線程,若是任務太重,最終會晚於終止。

GC外部的並行活動 GC Worker Other:該部分並不是GC的活動,而是JVM的活動致使佔用了GC暫停時間(例如JNI編譯)。

串行活動

代碼根更新 Code Root Fixup:根據轉移對象更新代碼根。

代碼根清理 Code Root Purge:清理代碼根集合表。

清除全局卡片標記 Clear CT:在任意收集週期會掃描CSet與RSet記錄的PRT,掃描時會在全局卡片表中進行標記,防止重複掃描。在收集週期的最後將會清除全局卡片表中的已掃描標誌。

選擇下次收集集合 Choose CSet:該部分主要用於併發標記週期後的年輕代收集、以及混合收集中,在這些收集過程當中,因爲有老年代候選分區的加入,每每須要對下次收集的範圍作出界定;但單純的年輕代收集中,全部收集的分區都會被收集,不存在選擇。

引用處理 Ref Proc:主要針對軟引用、弱引用、虛引用、final引用、JNI引用。當Ref Proc佔用時間過多時,可選擇使用參數-XX:ParallelRefProcEnabled激活多線程引用處理。G1但願應用能當心使用軟引用,由於軟引用會一直佔據內存空間直到空間耗盡時被Full GC回收掉;即便未發生Full GC,軟引用對內存的佔用,也會致使GC次數的增長。

引用排隊 Ref Enq:此項活動可能會致使RSet的更新,此時會經過記錄日誌,將關聯的卡片標記爲髒卡片。

卡片從新髒化 Redirty Cards:從新髒化卡片。

回收空閒巨型分區 Humongous Reclaim:G1作了一個優化:經過查看全部根對象以及年輕代分區的RSet,若是肯定RSet中巨型對象沒有任何引用,則說明G1發現了一個不可達的巨型對象,該對象分區會被回收。

釋放分區 Free CSet:回收CSet分區的全部空間,並加入到空閒分區中。

其餘活動 Other:GC中可能還會經歷其餘耗時很小的活動,如修復JNI句柄等。

併發標記週期後的年輕代收集

併發標記週期後的年輕代收集 Young Collection Following Concurrent Marking Cycle

當G1發起併發標記週期以後,並不會立刻開始混合收集。G1會先等待下一次年輕代收集,而後在該收集階段中,肯定下次混合收集的CSet(Choose CSet)。

混合收集週期

混合收集週期 Mixed Collection Cycle

單次的混合收集與年輕代收集並沒有二致。根據暫停目標,老年代的分區可能不能一次暫停收集中被處理完,G1會發起連續屢次的混合收集,稱爲混合收集週期(Mixed Collection Cycle)。G1會計算每次加入到CSet中的分區數量、混合收集進行次數,而且在上次的年輕代收集、以及接下來的混合收集中,G1會肯定下次加入CSet的分區集(Choose CSet),而且肯定是否結束混合收集週期。

轉移失敗的擔保機制 Full GC

轉移失敗的擔保機制 Full GC

轉移失敗(Evacuation Failure)是指當G1沒法在堆空間中申請新的分區時,G1便會觸發擔保機制,執行一次STW式的、單線程的Full GC。Full GC會對整堆作標記清除和壓縮,最後將只包含純粹的存活對象。參數-XX:G1ReservePercent(默認10%)能夠保留空間,來應對晉升模式下的異常狀況,最大佔用整堆50%,更大也無心義。

G1在如下場景中會觸發Full GC,同時會在日誌中記錄to-space-exhausted以及Evacuation Failure:

  1. 從年輕代分區拷貝存活對象時,沒法找到可用的空閒分區
  2. 從老年代分區轉移存活對象時,沒法找到可用的空閒分區
  3. 分配巨型對象時在老年代沒法找到足夠的連續分區

因爲G1的應用場合每每堆內存都比較大,因此Full GC的收集代價很是昂貴,應該避免Full GC的發生。

第五章 總結

G1是一款很是優秀的垃圾收集器,不只適合堆內存大的應用,同時也簡化了調優的工做。經過主要的參數初始和最大堆空間、以及最大容忍的GC暫停目標,就能獲得不錯的性能;同時,咱們也看到G1對內存空間的浪費較高,但經過首先收集儘量多的垃圾(Garbage First)的設計原則,能夠及時發現過時對象,從而讓內存佔用處於合理的水平。


參考資料

[1] Charlie H, Monica B, Poonam P, Bengt R. Java Performance Companion 
[2] 周志明. 深刻理解JVM虛擬機

 

 

原文地址:https://blog.csdn.net/coderlius/article/details/79272773

參考:http://ifeve.com/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3g1%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/

相關文章
相關標籤/搜索