譯文-G1收集器

原文出處:G1 – Garbage Firstjava

G1設計的一個重要目標是設置stop-the-world階段的持續時長和頻率,由於垃圾收集器可預測,可配置。事實上,G1是一款軟實時的收集器,意味着你能夠給它設置明確的運行目標。你能夠要求stop-the-world階段不超過 x milliseconds在給定的y milliseconds時長範圍以內,好比,在給定的s內不超過5s。G1收集器盡本身最大努力高几率實現目標(但不是必然,它會是硬實時)。算法

爲了實現它,G1創建在一系列的觀察上。首先,heap去不是必須Young和Old代分配連續的空間。相反,heap區分紅必定數量(表明性的2048)的小的heap區來分配對象。單個的區域多是Eden區,Survivor區,Old區。全部邏輯的Eden區和Survivor區合稱爲Young代,全部的Old區組合在一塊兒稱爲Old代:緩存

圖片描述

這容許GC避免一次回收整個heap區,取而代之遞增處理問題:每次只有collection set調用region的子集。每一個階段期間全部的Young region被回收,但一樣的只包含一部分old region:安全

圖片描述

G1的另外一個新特性是併發階段期間估算每個region裏包含存活數據的數量。這個被用於創建collection set:region包含的垃圾越多,越先被回收。所以名稱是:garbage-first 收集器。數據結構

爲了激活JVM中G1收集器,按照下面的命令執行你的應用:多線程

java -XX:+UseG1GC com.mypackages.MyExecutableClass

疏散(Evacuation)階段:Fully Young併發

在應用程序生命週期的開始階段,在併發階段執行以前,G1獲取不到任何附加信息,所以它的最初功能是full-yong模式。當Young代塞滿了,應用線程暫停,Young區的存活數據被複制到Survivor區域,任何空閒區域所以變成Survivor區。異步

複製對象過程被叫作疏散(Evacuation), 它的工做方式和咱們以前看到其餘Young收集器幾乎是同樣的。疏散階段full logs至關大,所以在第一次full-young 疏散階段咱們略去一些不相關的片斷。併發階段以後咱們會解釋大量細節。補充一點,因爲log記錄的全量尺寸,並行階段和「其餘」階段的細節被抽取成獨立的片斷:性能

0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs]1
    [Parallel Time: 13.9 ms, GC Workers: 8]2
        …3
    [Code Root Fixup: 0.0 ms]4
    [Code Root Purge: 0.0 ms]5
    [Clear CT: 0.1 ms]
    [Other: 0.4 ms]6
        …7
    [Eden: 24.0M(24.0M)->0.0B(13.0M) 8Survivors: 0.0B->3072.0K 9Heap: 24.0M(256.0M)->21.9M(256.0M)]10
     [Times: user=0.04 sys=0.04, real=0.02 secs] 11
  1. G1階段清理Young區域。JVM啓動以後的134ms階段開始,經過鍾牆時間檢測階段持續了0.0144s。優化

  2. 代表下列活動被8個並行GC線程實施耗費13.9ms(real time)。

  3. 省略部分,細節在下面的系列片斷。

  4. 釋放數據結構用於管理並行活動。一般應該是靠近zero。這一般順序完成。

  5. 清除更多數據結構,一般應該很是快,不是幾乎等於0。順序完成。

  6. 混雜其餘活動,它們的不少是並行的。

  7. 細節能夠看下面的章節。

  8. 階段先後的Eden區使用大小和容量大小。

  9. 階段先後被用於Survivor區的空間。

  10. 階段先後的heap區總使用大小和容量大小。

  11. GC時間期間,不一樣類別的時長:

    .user-回收期間GC線程消耗的總的cpu時間。
    .sys-調用系統或等待系統事件的耗費時長。
    .應用程序的停頓的時鐘時間。GC期間併發活動時長理論上接近(user time+sys time)GC線程數量消費的時長,這種狀況下用了8個線程。注意的是因爲一些活動不是並行執行,它會超過必定比率。

大多數重大事件被多個專用GC線程完成。它們的活動在以下面片斷的描述:

[Parallel Time: 13.9 ms, GC Workers: 8]1
     [GC Worker Start (ms)2: Min: 134.0, Avg: 134.1, Max: 134.1, Diff: 0.1]
    [Ext Root Scanning (ms)3: Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 1.2]
    [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
        [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
    [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
    [Code Root Scanning (ms)4: Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2]
    [Object Copy (ms)5: Min: 10.8, Avg: 12.1, Max: 12.6, Diff: 1.9, Sum: 96.5]
    [Termination (ms)6: Min: 0.8, Avg: 1.5, Max: 2.8, Diff: 1.9, Sum: 12.2]
        [Termination Attempts7: Min: 173, Avg: 293.2, Max: 362, Diff: 189, Sum: 2346]
    [GC Worker Other (ms)8: Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
    GC Worker Total (ms)9: Min: 13.7, Avg: 13.8, Max: 13.8, Diff: 0.1, Sum: 110.2]
    [GC Worker End (ms)10: Min: 147.8, Avg: 147.8, Max: 147.8, Diff: 0.0]
  1. 代表下列活動被8個並行GC線程實施耗費13.9ms(real time)。

  2. 線程開始活動的合計時間,在階段的開始時間匹配時間戳。若是Min和Max差異很大,它也許代表太多線程被使用或者JVM裏的GC進程CPU時間被機器上其餘進程盜用。

  3. 掃描外部(非heap)Root消耗的時間例如clasloader,JNI引用,JVM系統等等。展現消耗時間,「Sum」是cpu時間。

  4. 掃描來自真實code Root的時長:局部變量等等。

  5. 從回收區域複製存活對象花費的時間。

  6. GC線程肯定它們到達安全點消耗的時間,沒有多餘工做完成,而後終止。

  7. 工做線程嘗試終止的次數。實際上線程發現有任務須要完成的時候嘗試失敗,過早去終止。

  8. 其餘瑣碎的活動不值得在日誌裏獨立片斷展現。

  9. 任務線程總共花費的時間。

  10. 任務線程完成工做的時間戳。一般它們因該大值相等,另外一方面它也許顯示出太多線程無所事事,或者繁複的上下文工做。

此外,Evacuation階段期間一些混雜活動被執行。咱們會講解它們的一部分在下面的片斷。剩餘部分隨後講解。

圖片描述

  1. 混雜其餘的活動,大多數並行執行。

  2. 處理非強引用的時間:清除或者肯定不須要清理。

  3. 順序處理將剩下的非強引用從引用隊列中移除出去。

  4. 釋放收集集合裏面區域花費的時間以便它們適用於下一次分配。

併發標記

從上面章節看出G1借鑑了CMS的許多理念,所以能夠方便你充分理解以前的階段。雖然在一些方式上不盡相同,可是併發標記的目標很是相似。G1併發標記使用STAB(Snapshot-At-The-Beginning)方法,意味着在週期的開始階段標記全部存活對象,即便在收集期間已經調整。存活對象被容許創建在每一個區域(region)活躍性上,以便收集結合快速選擇。

這些信息隨後被用於執行Old代GC。它能夠徹底併發執行,一個僅僅包含垃圾的region被標記,或者一個Old region 「stop-the-world」evacuation階段包含垃圾和存活對象。

併發標記開始於heap區已使用空間足夠大。默認的,佔45%,可是能夠被JVM 選項InitiatingHeapOccupancyPercent 改變。相似CMS,G1併發標記有一些小階段組成,它們中一些徹底併發,一些則不得不暫停應用線程。

階段1:初始標記。這個階段標記全部從GC Root可達的對象。在CMS裏,他須要「stop-the-world」,可是在G1, Evacuation階段它能夠並行執行,所以它的上限事最小的。你能夠經過evacuation階段第一行添加「(initial-mark)」留意下GC日誌裏的這個階段:

1.631: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs]

階段2:Root region掃描。這個階段標記從所謂的root區域可達全部的存活對象,也就是標記週期中間沒有必要分配的非空的對象。由於移除標記週期中的填充會致使異常,這個階段必須在下一個階段開始之間完成。若是它提早開始,它會提早停止root掃描,而後等待完成。在目前的實現中,root區域在syrvivor區中:它們佔據Young代小部分空間,在下一個Evacuation 階段被禁止回收。

1.362: [GC concurrent-root-region-scan-start]
1.364: [GC concurrent-root-region-scan-end, 0.0028513 secs]

階段3:併發標記。這個階段和CMS的階段很是類似:它簡單統計對象,在特別的bitmap中標記能夠訪問的對象。爲了保證STAB語義論,爲了達到標記目的,經過可感知應用線程放棄先前的引用G1 GC須要全部的併發線程更新到對象統計模式。

經過使用寫屏障(Pre-Write barriers,不要混淆於Post-Write barriers,以及涉及多線程的memory barriers,隨後進行分析)。它們的職責是,每當G1併發標記期間你寫進一個字段,在所謂的log buffer裏面存儲以前結果,被併發標記線程處理。

1.364: [GC concurrent-mark-start]
1.645: [GC concurrent-mark-end, 0.2803470 secs]

階段4:再標記。這個階段須要「stop-the-world」,相似以前的CMS裏面看到,在標記階段完成。對於G1,對於遺留的部分它短暫的暫停應用線程去阻塞併發更新日誌和處理它們,標記併發標記開始的時候沒有標記的存活對象。這個階段執行一些額外的清理工做,例如,引用(查看Evacuation階段日誌)處理,或者卸載的class。

1.645: [GC remark 1.645: [Finalize Marking, 0.0009461 secs] 1.646: [GC ref-proc, 0.0000417 secs] 1.646: [Unloading, 0.0011301 secs], 0.0074056 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]

階段5:清除。最後的階段準備即將到來的Evacuation階段,計算heap區全部的存活對象,經過預期的GC效率排序這些region。它一般執行全部活動的整理工做,爲了下一次併發標記迭代維持內部狀態。

最後但一樣重要的是,包含再也不使用的對象的region在這個階段被回收。這個階段的一些部分是併發執行的,例如回收空region,大多數活躍性估算,但你在應用線程不干涉的期間一般須要短暫的「stop-the-world」階段區肯定方案。日誌「stop-the-world」階段和下圖相似:

1.652: [GC cleanup 1213M->1213M(1885M), 0.0030492 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]

一旦發現只包含垃圾對象的region,日誌格式會有些區別,相似於:

1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]
1.874: [GC concurrent-cleanup-start]
1.876: [GC concurrent-cleanup-end, 0.0014846 secs]

疏散階段:混合

併發清除能夠空出來整個old代是使人興奮,可是事實狀況是否是每次都這樣。併發標記已經成功完成以後,G1會安排一次混合回收,不只僅是回收young region的垃圾,還把old region的一部分放進collection set中。

一次混合疏散階段不是常常緊隨併發標記階段結束以後。有一系列的規則和試探影響這個階段。例如,併發空出來old region中的大塊區域是可能的,然而根本沒有必要去作。

它們可能所以在併發標記結束和混合evacuation階段之間簡單發生一系列full-young evacuation階段。

old區準確的對象被添加進collection set,保證其添加順序,根據一些規則挑選出順序。這些包含了爲了達到應用程序的軟實時性能目標。併發標記期間,活躍的和GC 效率的數據被回收,還有一些JVM配置選項。混合回收進程和初期分析的full-young gc同樣龐大,可是此次咱們會包含remembered set的子集。

Remembered set運行heap不一樣region使用獨立的收集器。例如,當回收區域A,B和C,咱們必須知道D和E中任何一個引用它們,去肯定它們的活躍性。可是統計整個heap區花費很長時間,銷燬整個增量收集,所以最佳化被破壞。很像爲了使用其餘GC算法獨立手機Young region咱們使用卡表( Card Table),在G1中咱們使用Remembered Sets。

正如上面插圖展現那樣,每個region有一個remembered sets列出外部引用指向這個區域。這些被看做額外的GC Roots。注意的是併發標記期間old區域被判斷爲垃圾的對象,即便外部引用它們會被忽略:那種狀況下被看成垃圾的參照圖:

圖片描述

下一步操做和其餘收集器同樣:多個並行GC線程計算出哪些對象存活,哪些是垃圾:

圖片描述

最後,存活對象被移到Survior區域中,若是有必要則新建。空的orgion被釋放出來,用戶再次存儲對象:

圖片描述

爲了維護Remembered Sets,應用運行期間,每當寫入操做執行的時候觸發一個Post-Write Barrier。若是一個引用跨越region,也就是一個region指向另外一個region,目標region的 Remembered Set存入相對應的entry中。爲了減小write barrier,將card放進Remember Set過程異步執行,突出性能最優化。可是基本上將dirty card信息放進本地緩存的方式存入Write barrier,一個專門GC線程將信息引用region的Remember set中。

混合模式中,對照fully young模式log展現一些有趣的方面:

[Update RS (ms)1: Min: 0.7, Avg: 0.8, Max: 0.9, Diff: 0.2, Sum: 6.1]
[Processed Buffers2: Min: 0, Avg: 2.2, Max: 5, Diff: 5, Sum: 18]
[Scan RS (ms)3: Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]
[Clear CT: 0.2 ms]4
[Redirty Cards: 0.1 ms]5
  1. 自併發執行Remember Set之後,我必須確保真正收集開始以前still-buffered cards被執行。若是數量不少,併發GC線程沒法負載。他多是,舉例來講,勢不可擋的到來的字段修改
    的數量,或者CPU資源不足。

  2. 每個任務線程操做local buffer的數量。

  3. 從Remember Set中掃描引用的數量。

  4. 清理card table中的card花費的時間。Remember set經過簡單移除「dirty」(表示filed被修改)狀態進行清理工做。

  5. card table超着ditry card的佔用位置花費的時間。GC本身操做經過heap中發生突變被定義爲位置佔用,例如引用隊列。

總結
這個應該創建在充分理解G1若是工做基礎之上,這些固然爲了簡介,須要咱們忽略至關多的一些實現細節,像humongous objects的細節。綜合考慮,G1是HotSpot中現有的最早進的收集器產品,在G1上,他被HotSpot的工程師無所不用其極地改進,在即將到來的Java 新版本。

正如他我所看到的,G1修正了CMS的廣爲人知的問題,從階段可預測到heap碎片。使得應用再也不受限於CPU利用率,可是對個別選項十分敏感。G1極可能是對HotSpot用戶來講最好的選擇,尤爲是運行最新版本的Java。然而,這性能升不是毫無代價:G1吞吐量歸功於附加的write barrier和更多後臺活動線程。若是應用是吞吐量優先或者CPU使用率100%,不關注個別階段,CMS,甚至Parallel或許是更好的選擇。

惟一可行選擇合適的GC算法和設置的方式經過嘗試和錯誤,可是我還在下一章節給出通常的參考。

注意的是G1頗有多是java 9默認的GC收集器:http://openjdk.java.net/jeps/248

相關文章
相關標籤/搜索