G1 收集器

基礎知識

性能指標

在調優Java應用程序時,重點一般放在兩個主要目標上:響應性吞吐量html

 響應性Responsiveness 是指應用程序對請求的數據作出響應的速度:java

  • 桌面用戶界面對事件的響應速度
  • 網站返回頁面的速度
  • 數據庫查詢的返回速度

 吞吐量Throughput 專一於最大程度地提升應用程序在特定時間段內的工做量:算法

  • 在給定時間內完成的事務次數
  • 批處理程序在一小時內能夠完成的做業數
  • 一小時內能夠完成的數據庫查詢數

較長的暫停時間Pause Time對於注重響應性的應用程序是不可接受的,但對於注重吞吐量的應用程序來講能夠接受的。前者重點是在短期內作出響應,後者則側重與長時間運行的處理效率。數據庫

GC 基礎

GC Root

可達性分析是 Java GC 算法的基礎,基本思路就是以一系列名爲 GC Roots 對象做爲起始點,經過引用關係遍歷對象圖,若是一個對象到 GC Roots 間沒有任何可達路徑相連時,則說明此對象能夠被回收。segmentfault

能夠做爲 GC Roots 的對象:緩存

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 本地方法棧中JNI(即通常說的native方法)中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象

三色標記

可達性分析中重要的一環就是遍歷整個堆,並標記其中的存活對象。一種經常使用的標記算法是 三色標記法tri-color marking服務器

每一個對象可能爲如下 3 種顏色之一:數據結構

  • white — 未被標記
  • gray — 自己已標記,但部分引用的對象完成標記(動圖的黃色對象)
  • black — 自己已標記,且全部引用的對象完成標記(動圖的藍色對象)

標記算法從 GC Roots 出發遍歷堆,可達對象先標記 gray,而後再標記 爲 black。併發

遍歷完成以後全部可達對象都是 black 的,此時全部標記爲 white 的對象都是能夠回收的。oracle

當實現併發標記算法時,必須防止 white 對象被漏標,不然可能致使不應回收的對象被回收。


分代收集

傳統垃圾收集器將堆分紅三個部分:年輕代YoungGen = Eden + Survivor,老年代OldGen和永久代PermGen,每一個區域內存連續且大小固定。

  • 年輕代:一次性使用的臨時對象(例如:方法中構造的臨時對象)
  • 老年代:被長期引用的常駐對象(例如:緩存對象、單例對象)
  • 永久代:JVM 運行過程當中一直存在的對象(例如:字符串常量、類信息)

將堆內存進行劃分後,能夠按照對象生命週期長短,在不一樣區域使用不一樣的回收算法,提升 GC 的效率。


算法分類

Mark and Sweep標記-清除

 用一個空閒列表free-list記錄失效對象佔用的內存區域,方便後續從新分配給新對象。

  • 回收原理簡單,GC 停頓時間短
  • 維護空閒列表須要必定的空間開銷
  • 內存碎片較多,可能致使內存分配失敗

Mark-Sweep-Compact標記-整理

 將全部存活對象移動到內存區域的開頭,剩餘的連續內存區域都是可用的空閒空間。

  • 經過指針碰撞查找空閒空間,分配速度快
  • 內存碎片少,內存分配失敗機率低
  • 複製對象會致使較長時間的 GC 停頓

Mark and Copy標記-複製

 將內存劃分爲活動區間空閒區間,前者用於動態分配對象,後者用於容納 GC 存活對象。
 GC 時只需將存活對象從前者複製到後者,而後交換二者的角色便可。

  • 標記和複製在同一階段同時進行,當存活對象少時回收效率極高
  • 須要預留一個空閒空間用於容納存活對象,形成內存浪費

CMS 回顧

CMS Concurrent Mark-Sweep 是一個採用 標記-清除 算法的老年代收集器。
它經過與應用程序線程併發執行大多數垃圾回收工做,來最大程度地減小因爲 GC 致使的暫停。

一般狀況下,CMS 收集器不會複製或壓縮活動對象,這意味着無需移動活動對象便可完成垃圾回收。
然而過多的內存碎片可能形成分配失敗,最終致使 FullGC。能夠經過分配更大的堆來規避這一問題。

CMS 對老年代的回收能夠分爲如下幾個步驟:

  • Initial Mark (STW) 初始標記

    • 標記 GC Roots 直接可達的老年代對象
    • 遍歷新生代存活對象,標記直接可達的老年代對象

  • Concurrent Mark 併發標記

    GC 線程遍歷 Initial Mark 階段標記出來存活的老年代對象,而後遞歸標記這些可達的對象。

    該階段與應用線程併發運行,期間會發生新生代對象晉升、老年代對象引用關係更新,須要對這些對象進行從新標記,避免發生遺漏。

    CMS 用一個card-table管理老年代,併發標記過程當中,某個對象的引用關係發生了變化,則將對象所在的內存塊標記爲 Dirty Card

    CMS 使用增量更新incremental update解決併發修改致使的漏標問題:把 black 對象從新標記爲 grey,下次從新掃描其引用。

  • Preclean 預清理

    這一階段主要是處理 Concurrent Mark 階段中引用關係改變,致使沒有標記到的存活對象的。經過併發地從新掃描這些對象,預清理階段能夠減小 Remark 階段的 STW。

    這個階段會處理前一個階段被標記爲 Dirty Card 的部分,將其中變化了的對象做爲 GC Root 再進行掃描並從新標記。

  • Abortable Preclean 可終止的預清理

    這個階段做用與 Preclean 相似,但能夠經過設置 掃描時長(默認5秒)或 Eden 區使用佔比(默認50%)控制本階段的結束時機。

    增長這一階段的緣由,是期待這期間能發生一次 YoungGC 清理無效的年輕代對象,減小 Remark 階段掃描年輕代的時間。

  • Remark (STW) 從新標記:

    這個階段同時掃描 YoungGen 與 OldGen,從新標記整個老年代中全部存活對象。

    因爲以前的 Concurrent MarkPreclean 階段是與用戶線程併發執行的,年輕代對老年代的引用可能已經發生了改變,Remark 要花不少時間處理這些改變,會致使長時間的 STW。

    此外,即便新生代的對象已經不可達了,CMS 也會使用這些不可達的對象當作的 GC Roots 來掃描老年代,致使部分失效的老年代對象沒法被及時回收。

    能夠加入參數 -XX:+CMSScavengeBeforeRemark,在從新標記以前,先執行一次 YoungGC,回收掉年輕代的對象無用的對象。這樣進行年輕代掃描時,只須要掃描 Survivor 區的對象便可,通常 Survivor 區很是小,這大大減小了掃描時間。

  • Concurrent Sweep 併發清理

    至此,老年代全部存活的對象已經被標記完成。這個階段主要是清除那些沒有標記的對象而且回收空間。

    被回收的空間會被添加到 空閒列表中,以供之後分配。這一過程可能會對空閒空間進行合併,可是不會移動存活對象。

    因爲該階段是與應用線程併發運行的,天然就還會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,沒法在當次收集中處理掉它們。只好留待下一次GC時再清理掉。這一部分垃圾就稱爲 浮動垃圾

  • Resetting 重置

    清除數據結構,並重置定時器,爲下一輪 GC 作準備。

G1 算法

設計目的

G1 Garbage-First 是一種服務器端的垃圾收集器:

  • 能夠與應用程序線程並行運行,減小 STW
  • 整理空閒空間減小內存碎片,但不引入較長的 GC 暫停時間
  • 提供可預測的GC暫停時間,無需犧牲不少吞吐量

G1 可以在大內存的多處理器計算機上,保證 GC 暫停時間可控,並實現高吞吐量。

其最終目的是取代 CMS 成爲服務端 GC 更好的解決方案:

  • 採用 標記-整理 算法,能夠避免使用細粒度的空閒列表進行分配。簡化了收集器設計並消除了潛在的碎片問題。
  • 使用 增量回收incremental collecting 算法,其 GC 暫停時間比 CMS 更具可預測性,並容許用戶指按期望的暫停時間。

基本概念

G1 將堆劃分爲一組大小相等的且連續的堆區域Region

G1 中新生代與老年代再也不連續,每一個區域能夠在 EdenSurvivorOld 之間切換角色。此外,還有一類被稱爲 Humongous 的巨型區域,用於容納體積 ≥ 標準區域大小的50%的對象。JVM 一般會將內存劃分爲 2000個區域,每一個大小從 1 到 32Mb 不等,由 JVM 在啓動時經過 -XX:G1HeapRegionSize 指定。

每一個區域會被進一步細分紅多個卡片Card,每一個大小爲 512Kb,用於實現細粒度的引用統計。

分區設計能夠避免一次收集整個堆,每次 GC 只收集區域的一個子集 CSetcollection set,其中必然包含全部 Young 區域,同時可能包括部分 Old 區域:

根據回收區域的不一樣,能夠將 GC 分爲:

  • YoungGCCSet 只包含 Young 區域
  • MixedGCCSet 同時包含 YoungOld 區域
  • FullGC: 回收整個堆(可用空間耗盡時觸發,單線程執行)

G1 根據存活對象的字節數統計每一個區域的 活躍度liveness,而後根據指望停頓時間來肯定該 CSet 的大小,並保證那些垃圾多(活躍度低)的區域會被優先回收,故此得名 垃圾優先

G1 的執行過程能夠表示爲由 3 個階段組成的循環:


Young GC

堆中一開始只有 YoungGen,所以只會觸發 YoungGC,將 EdenSurvivor 區域中的活動對象複製到另外一個空閒的 Survivor 區域。

G1 中將 將存活對象複製到其餘區域 的過程稱爲 疏散Evacuation。爲了減小停頓時間,疏散工做由多個 GC 線程並行完成。

YoungGC 過程當中會根據預期目標停頓時間 -XX:MaxGCPauseMillis 動態調整新生代的大小,經過 -XX:G1NewSizePercent 參數能夠人爲干預這一過程,但會讓預期停頓時間參數失效。

當堆的總體佔用空間足夠大時(超過45%),就會進入 Concurrent Marking 階段。經過 -XX:InitiatingHeapOccupancyPercent 選項能夠配置這一行爲。

Concurrent Marking

與 CMS 相似,G1 中的併發標記包括多個階段,其中一些階段是併發的,另外一些階段則會 STW。

  • Initial Mark (STW) 初始標記

    掃描並標記 GC Root 對象直接可達的老年代存活對象。

    Initial Mark 並無獨立的執行階段,而是嵌入 YoungGC 中執行的,其停頓時間會被分攤,所以實際的開銷很是低。


  • Root Region Scan 掃描根區域

    掃描 Root Region 並標記全部可達的老年代存活對象。

    此處的 Root Region 就是先前 YoungGC 中生成的 Survivor 區域,其包含的對象都會被視爲 GC Root

    爲了不移動對象對標記產生影響,該過程必須在下次 YongGC 啓動前完成。

  • Concurrent Mark 併發標記

    啓動併發標記線程,掃描並標記整個堆中的存活對象(線程數能夠經過 -XX:ConcGCThread 進行配置)。

    爲了不重複標記,G1 使用 SATBsnapshot-at-the-beginning算法解決漏標問題:

    應用線程對在 Concurrent Mark 執行期間進行的全部併發更新,都應保留先前的已知標記信息。

    該約束是經過預寫屏障pre-write barrier實現:

    Concurrent Mark 掃描過程當中,當應用線程修改某個字段時,會將先前的引用對象存儲在 日誌緩衝區 log buffers中,而後交由併發標記線程處理。

    爲了不移動對象對標記產生影響,該過程必須在下次 YoungGC 啓動前完成。全部的標記任務必須在堆滿前完成,若是堆滿前沒有完成標記任務,則會觸發擔保機制,經歷一次長時間的串行 FullGC

  • Remark (STW) 從新標記

    啓動並行標記線程,完成對整個堆中存活對象的標記(線程數能夠經過 -XX:ParallelGCThread 進行配置)。

    該階段會暫停全部應用線程,避免發生引用更新,並完成對SATB 日誌緩衝區中剩餘對象的標記,找出全部未被訪問的存活對象。

    該階段還執行一些額外的清理操做,例如:

    • 卸載不可達的類(經過 -XX:+ClassUnloadingWithConcurrentMark 開啓)
    • 處理引用對象(弱引用、軟引用、虛引用、最終引用)

  • Cleanup 清理垃圾

    整理統計信息並識別出高收益的老年代分區,爲 MixedGC 作準備。

    主要工做有:

    • RSet 梳理(後續說明)
    • 識別回收收益高的老年代分區 (基於釋放空間和暫停目標)
    • 直接回收的沒有活躍對象的空閒分區

    此外還會執行一些清理工做,爲下一次 Concurrent Marking 作好準備。

Mixed GC

MixedGC 主要流程與 YoungGC 相似,不一樣的地方在於 CSet 中包含了 Old 區域。

須要注意的是,Concurrent Marking 結束後,並不必定會當即觸發 MixedGC,中間可能會穿插屢次的 YoungGC

當收集某個區域時,咱們必須知道是否有來自非收集區域引用,來肯定它們的活動性:

  • 從非收集區域到收集區域的 incoming reference 是重要的(被非收集區引用的對象必須存活)
  • 從收集區域到非收集區域的 outgoing reference 是可忽略的(非收集區域不參與GC)

但查找整個堆很是耗時,同時也失去了增量收集的優點。爲了解決這一問題,G1 爲每一個區域維護了一個 RSetremembered set,用於記憶從其餘區域指向本身的引用。


收集過程

在執行收集時,RSet 中引用信息會扮演局部 GC Roots 的角色,避免耗時的引用查找,保證每一個區域的 GC 可以獨立進行:

注意,象若是 Old 區域中對在 Concurrent Marking 階段被肯定爲垃圾,即便有外部引用,該對象也會被做爲垃圾回收。

接下來發生的事情與其餘收集器所作的相同:多個並行GC線程找出哪些對象是活動的,哪些對象是垃圾:

最後,釋放空閒區域,將活動對象移到 Survivor 區域,並在必要時建立新對象:


RSet 維護

爲了維護 RSet,在應用線程對字段執行寫操做時,會觸發寫後屏障post-write barrier

若是更新後的引用是跨區域的(即從一個區域指向另外一個區域),則對應的條目將出如今目標區域的 RSet 中。

爲了減小寫屏障帶來的開銷,該過程是異步的:

應用線程只負責把更新字段所在的 Card 信息插入一個 DCQ Dirty Card Queue,而後由 Refine 線程將其拾取並將信息傳播到被引用區域的 RSet。

若是應用線程插入速度過快,會致使 Refine 線程來不及處理,那麼應用線程將接管 RSet 更新的任務,從而致使性能降低。

總結

併發標記增量收集 是 G1 實現高性能與可預測回收的關鍵。

對於 CPU 資源充足且對延遲敏感的服務端應用來講,G1 算法可以在大堆上提供良好的響應速度。

做爲代價,額外的寫屏障與更活躍GC線程,會對應用的吞吐量產生負面影響。


參考資料

相關文章
相關標籤/搜索