Java Hotspot G1 GC的一些關鍵技術

前言

G1 GC,全稱Garbage-First Garbage Collector,通過-XX:+UseG1GC參數來啓用,作爲體驗版隨着JDK 6u14版本面世,在JDK 7u4版本發行時被正式推出,相信熟悉JVM的同學們都不會對它感到陌生。在JDK 9中,G1被提議設置爲默認垃圾收集器(JEP 248)。在官網中,是這樣描述G1的:

The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that:

  • Can operate concurrently with applications threads like the CMS collector.
  • Compact free space without lengthy GC induced pause times.
  • Need more predictable GC pause durations.
  • Do not want to sacrifice a lot of throughput performance.
  • Do not require a much larger Java heap.

從官網的描述中,我們知道G1是一種服務器端的垃圾收集器,應用在多處理器和大容量內存環境中,在實現高吞吐量的同時,儘可能的滿足垃圾收集暫停時間的要求。它是專門針對以下應用場景設計的:

  • 像CMS收集器一樣,能與應用程序線程併發執行。
  • 整理空閒空間更快。
  • 需要GC停頓時間更好預測。
  • 不希望犧牲大量的吞吐性能。
  • 不需要更大的Java Heap。

G1收集器的設計目標是取代CMS收集器,它同CMS相比,在以下方面表現的更出色:

  • G1是一個有整理內存過程的垃圾收集器,不會產生很多內存碎片。
  • G1的Stop The World(STW)更可控,G1在停頓時間上添加了預測機制,用戶可以指定期望停頓時間。

有了以上的特性,難怪有人說它是一款駕馭一切的垃圾收集器(G1: One Garbage Collector To Rule Them All)。本文帶大家來了解一下G1 GC的一些關鍵技術,爲能正確的使用它,做好理論基礎的鋪墊。

G1中幾個重要概念

在G1的實現過程中,引入了一些新的概念,對於實現高吞吐、沒有內存碎片、收集時間可控等功能起到了關鍵作用。下面我們就一起看一下G1中的這幾個重要概念。

Region

傳統的GC收集器將連續的內存空間劃分爲新生代、老年代和永久代(JDK 8去除了永久代,引入了元空間Metaspace),這種劃分的特點是各代的存儲地址(邏輯地址,下同)是連續的。如下圖所示:
傳統GC內存佈局

而G1的各代存儲地址是不連續的,每一代都使用了n個不連續的大小相同的Region,每個Region佔有一塊連續的虛擬內存地址。如下圖所示:
g1 GC內存佈局

在上圖中,我們注意到還有一些Region標明瞭H,它代表Humongous,這表示這些Region存儲的是巨大對象(humongous object,H-obj),即大小大於等於region一半的對象。H-obj有如下幾個特徵:

  • H-obj直接分配到了old gen,防止了反覆拷貝移動。
  • H-obj在global concurrent marking階段的cleanup 和 full GC階段回收。
  • 在分配H-obj之前先檢查是否超過 initiating heap occupancy percent和the marking threshold, 如果超過的話,就啓動global concurrent marking,爲的是提早回收,防止 evacuation failures 和 full GC。

爲了減少連續H-objs分配對GC的影響,需要把大對象變爲普通的對象,建議增大Region size。

一個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值範圍從1M到32M,且是2的指數。如果不設定,那麼G1會根據Heap大小自動決定。相關的設置代碼如下:

  1. // share/vm/gc_implementation/g1/heapRegion.cpp
  2. // Minimum region size; we won’t go lower than that.
  3. // We might want to decrease this in the future, to deal with small
  4. // heaps a bit more efficiently.
  5. #define MIN_REGION_SIZE ( 1024 * 1024 )
  6. // Maximum region size; we don’t go higher than that. There’s a good
  7. // reason for having an upper bound. We don’t want regions to get too
  8. // large, otherwise cleanup’s effectiveness would decrease as there
  9. // will be fewer opportunities to find totally empty regions after
  10. // marking.
  11. #define MAX_REGION_SIZE ( 32 * 1024 * 1024 )
  12. // The automatic region size calculation will try to have around this
  13. // many regions in the heap (based on the min heap size).
  14. #define TARGET_REGION_NUMBER 2048
  15. void HeapRegion::setup_heap_region_size(size_t initial_heap_size, size_t max_heap_size) {
  16. uintx region_size = G1HeapRegionSize;
  17. if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {
  18. size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;
  19. region_size = MAX2(average_heap_size / TARGET_REGION_NUMBER,
  20. (uintx) MIN_REGION_SIZE);
  21. }
  22. int region_size_log = log2_long((jlong) region_size);
  23. // Recalculate the region size to make sure it’s a power of
  24. // 2. This means that region_size is the largest power of 2 that’s
  25. // <= what we’ve calculated so far.
  26. region_size = ((uintx)1 << region_size_log);
  27. // Now make sure that we don’t go over or under our limits.
  28. if (region_size < MIN_REGION_SIZE) {
  29. region_size = MIN_REGION_SIZE;
  30. } else if (region_size > MAX_REGION_SIZE) {
  31. region_size = MAX_REGION_SIZE;
  32. }
  33. }

SATB

全稱是Snapshot-At-The-Beginning,由字面理解,是GC開始時活着的對象的一個快照。它是通過Root Tracing得到的,作用是維持併發GC的正確性。
那麼它是怎麼維持併發GC的正確性的呢?根據三色標記算法,我們知道對象存在三種狀態:

  • 白:對象沒有被標記到,標記階段結束後,會被當做垃圾回收掉。
  • 灰:對象被標記了,但是它的field還沒有被標記或標記完。
  • 黑:對象被標記了,且它的所有field也被標記完了。

由於併發階段的存在,Mutator和Garbage Collector線程同時對對象進行修改,就會出現白對象漏標的情況,這種情況發生的前提是:

  • Mutator賦予一個黑對象該白對象的引用。
  • Mutator刪除了所有從灰對象到該白對象的直接或者間接引用。

對於第一個條件,在併發標記階段,如果該白對象是new出來的,並沒有被灰對象持有,那麼它會不會被漏標呢?Region中有兩個top-at-mark-start(TAMS)指針,分別爲prevTAMS和nextTAMS。在TAMS以上的對象是新分配的,這是一種隱式的標記。對於在GC時已經存在的白對象,如果它是活着的,它必然會被另一個對象引用,即條件二中的灰對象。如果灰對象到白對象的直接引用或者間接引用被替換了,或者刪除了,白對象就會被漏標,從而導致被回收掉,這是非常嚴重的錯誤,所以SATB破壞了第二個條件。也就是說,一個對象的引用被替換時,可以通過write barrier 將舊引用記錄下來。

  1. // share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.hpp
  2. // This notes that we don't need to access any BarrierSet data
  3. // structures, so this can be called from a static context.
  4. template <class T> static void write_ref_field_pre_static(T* field, oop newVal) {
  5. T heap_oop = oopDesc::load_heap_oop(field);
  6. if (!oopDesc::is_null(heap_oop)) {
  7. enqueue(oopDesc::decode_heap_oop(heap_oop));
  8. }
  9. }
  10. // share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.cpp
  11. void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
  12. // Nulls should have been already filtered.
  13. assert(pre_val->is_oop(true), "Error");
  14. if (!JavaThread::satb_mark_queue_set().is_active()) return;
  15. Thread* thr = Thread::current();
  16. if (thr->is_Java_thread()) {
  17. JavaThread* jt = (JavaThread*)thr;
  18. jt->satb_mark_queue().enqueue(pre_val);
  19. } else {
  20. MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag);
  21. JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
  22. }
  23. }

SATB也是有副作用的,如果被替換的白對象就是要被收集的垃圾,這次的標記會讓它躲過GC,這就是float garbage。因爲SATB的做法精度比較低,所以造成的float garbage也會比較多。

RSet

全稱是Remembered Set,是輔助GC過程的一種結構,典型的空間換時間工具,和Card Table有些類似。還有一種數據結構也是輔助GC的:Collection Set(CSet),它記錄了GC要收集的Region集合,集合裏的Region可以是任意年代的。在GC的時候,對於old->young和old->old的跨代對象引用,只要掃描對應的CSet中的RSet即可。
邏輯上說每個Region都有一個RSet,RSet記錄了其他Region中的對象引用本Region中對象的關係,屬於points-into結構(誰引用了我的對象)。而Card Table則是一種points-out(我引用了誰的對象)的結構,每個Card 覆蓋一定範圍的Heap(一般爲512Bytes)。G1的RSet是在Card Table的基礎上實現的:每個Region會記錄下別的Region有指向自己的指針,並標記這些指針分別在哪些Card的範圍內。 這個RSet其實是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裏面的元素是Card Table的Index。

下圖表示了RSet、Card和Region的關係(出處):
Remembered Sets

上圖中有三個Region,每個Region被分成了多個Card,在不同Region中的Card會相互引用,Region1中的Card中的對象引用了Region2中的Card中的對象,藍色實線表示的就是points-out的關係,而在Region2的RSet中,記錄了Region1的Card,即紅色虛線表示的關係,這就是points-into。
而維繫RSet中的引用關係靠post-write barrier和Concurrent refinement threads來維護,操作僞代碼如下(出處):

  1. void oop_field_store(oop* field, oop new_value) {
  2. pre_write_barrier(field); // pre-write barrier: for maintaining SATB invariant
  3. *field = new_value; // the actual store
  4. post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
  5. }

post-write barrier記錄了跨Region的引用更新,更新日誌緩衝區則記錄了那些包含更新引用的Cards。一旦緩衝區滿了,Post-write barrier就停止服務了,會由Concurrent refinement threads處理這些緩衝區日誌。
RSet究竟是怎麼輔助GC的呢?在做YGC的時候,只需要選定young generation region的RSet作爲根集,這些RSet記錄了old->young的跨代引用,避免了掃描整個old generation。 而mixed gc的時候,old generation中記錄了old->old的RSet,young->old的引用由掃描全部young generation region得到,這樣也不用掃描全部old generation region。所以RSet的引入大大減少了GC的工作量。

Pause Prediction Model

Pause Prediction Model 即停頓預測模型。它在G1中的作用是:

G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.

G1 GC是一個響應時間優先的GC算法,它與CMS最大的不同是,用戶可以設定整個GC過程的期望停頓時間,參數-XX:MaxGCPauseMillis指定一個G1收集過程目標停頓時間,默認值200ms,不過它不是硬性條件,只是期望值。那麼G1怎麼滿足用戶的期望呢?就需要這個停頓預測模型了。G1根據這個模型統計計算出來的歷史數據來預測本次收集需要選擇的Region數量,從而儘量滿足用戶設定的目標停頓時間。
停頓預測模型是以衰減標準偏差爲理論基礎實現的:

  1. // share/vm/gc_implementation/g1/g1CollectorPolicy.hpp
  2. double get_new_prediction(TruncatedSeq* seq) {
  3. return MAX2(seq->davg() + sigma() * seq->dsd(),
  4. seq->davg() * confidence_factor(seq->num()));
  5. }

在這個預測計算公式中:davg表示衰減均值,sigma()返回一個係數,表示信賴度,dsd表示衰減標準偏差,confidence_factor表示可信度相關係數。而方法的參數TruncateSeq,顧名思義,是一個截斷的序列,它只跟蹤了序列中的最新的n個元素。

在G1 GC過程中,每個可測量的步驟花費的時間都會記錄到TruncateSeq(繼承了AbsSeq)中,用來計算衰減均值、衰減變量,衰減標準偏差等:

  1. // src/share/vm/utilities/numberSeq.cpp
  2. void AbsSeq::add(double val) {
  3. if (_num == 0) {
  4. // if the sequence is empty, the davg is the same as the value
  5. _davg = val;
  6. // and the variance is 0
  7. _dvariance = 0.0;
  8. } else {
  9. // otherwise, calculate both
  10. _davg = (1.0 - _alpha) * val + _alpha * _davg;
  11. double diff = val - _davg;
  12. _dvariance = (1.0 - _alpha) * diff * diff + _alpha * _dvariance;
  13. }
  14. }

比如要預測一次GC過程中,RSet的更新時間,這個操作主要是將Dirty Card加入到RSet中,具體原理參考前面的RSet。每個Dirty Card的時間花費通過_cost_per_card_ms_seq來記錄,具體預測代碼如下:

  1. // share/vm/gc_implementation/g1/g1CollectorPolicy.hpp
  2. double predict_rs_update_time_ms(size_t pending_cards) {
  3. return (double) pending_cards * predict_cost_per_card_ms();
  4. }
  5. double predict_cost_per_card_ms() {
  6. return get_new_prediction(_cost_per_card_ms_seq);
  7. }

get_new_prediction就是我們開頭說的方法,現在大家應該基本明白停頓預測模型的實現原理了。

GC過程

講完了一些基本概念,下面我們就來看看G1的GC過程是怎樣的。

G1 GC模式

G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是完全Stop The World的。

  • Young GC:選定所有年輕代裏的Region。通過控制年輕代的region個數,即年輕代內存大小,來控制young GC的時間開銷。
  • Mixed GC:選定所有年輕代裏的Region,外加根據global concurrent marking統計得出收集收益高的若干老年代Region。在用戶指定的開銷目標範圍內儘可能選擇收益高的老年代Region。

由上面的描述可知,Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC實在無法跟上程序分配內存的速度,導致老年代填滿無法繼續進行Mixed GC,就會使用serial old GC(full GC)來收集整個GC heap。所以我們可以知道,G1是不提供full GC的。

上文中,多次提到了global concurrent marking,它的執行過程類似CMS,但是不同的是,在G1 GC中,它主要是爲Mixed GC提供標記服務的,並不是一次GC過程的一個必須環節。global concurrent marking的執行過程分爲四個步驟:

  • 初始標記(initial mark,STW)。它標記了從GC Root開始直接可達的對象。
  • 併發標記(Concurrent Marking)。這個階段從GC Root開始對heap中的對象標記,標記線程與應用程序線程並行執行,並且收集各個Region的存活對象信息。
  • 最終標記(Remark,STW)。標記那些在併發標記階段發生變化的對象,將被回收。
  • 清除垃圾(Cleanup)。清除空Region(沒有存活對象的),加入到free list。

第一階段initial mark是共用了Young GC的暫停,這是因爲他們可以複用root scan操作,所以可以說global concurrent marking是伴隨Young GC而發生的。第四階段Cleanup只是回收了沒有存活對象的Region,所以它並不需要STW。

Young GC發生的時機大家都知道,那什麼時候發生Mixed GC呢?其實是由一些參數控制着的,另外也控制着哪些老年代Region會被選入CSet。

  • G1HeapWastePercent:在global concurrent marking結束之後,我們可以知道old gen regions中有多少空間要被回收,在每次YGC之後和再次發生Mixed GC之前,會檢查垃圾佔比是否達到此參數,只有達到了,下次纔會發生Mixed GC。
  • G1MixedGCLiveThresholdPercent:old generation region中的存活對象的佔比,只有在此參數之下,纔會被選入CSet。
  • G1MixedGCCountTarget:一次global concurrent marking之後,最多執行Mixed GC的次數。
  • G1OldCSetRegionThresholdPercent:一次Mixed GC中能被選入CSet的最多old generation region數量。

除了以上的參數,G1 GC相關的其他主要的參數有:

參數含義
-XX:G1HeapRegionSize=n設置Region大小,並非最終值
-XX:MaxGCPauseMillis設置G1收集過程目標時間,默認值200ms,不是硬性條件
-XX:G1NewSizePercent新生代最小值,默認值5%
-XX:G1MaxNewSizePercent新生代最大值,默認值60%
-XX:ParallelGCThreadsSTW期間,並行GC線程數
-XX:ConcGCThreads=n併發標記階段,並行執行的線程數
-XX:InitiatingHeapOccupancyPercent設置觸發標記週期的 Java 堆佔用率閾值。默認值是45%。這裏的java堆佔比指的是non_young_capacity_bytes,包括old+humongous

GC日誌

G1收集器的日誌與其他收集器有很大不同,源於G1獨立的體系架構和數據結構,下面這兩段日誌來源於美團點評的CRM系統線上生產環境。

Young GC日誌

我們先來看看Young GC的日誌:

  1. {Heap before GC invocations=12 (full 1):
  2. garbage-first heap total 3145728K, used 336645K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
  3. region size 1024K, 172 young (176128K), 13 survivors (13312K)
  4. Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K
  5. class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K
  6. 2014-11-14T17:57:23.654+0800: 27.884: [GC pause (G1 Evacuation Pause) (young)
  7. Desired survivor size 11534336 bytes, new threshold 15 (max 15)
  8. - age 1: 5011600 bytes, 5011600 total
  9. 27.884: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 1461, predicted base time: 35.25 ms, remaining time: 64.75 ms, target pause time: 100.00 ms]
  10. 27.884: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 159 regions, survivors: 13 regions, predicted young region time: 44.09 ms]
  11. 27.884: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 159 regions, survivors: 13 regions, old: 0 regions, predicted pause time: 79.34 ms, target pause time: 100.00 ms]
  12. , 0.0158389 secs]
  13. [Parallel Time: 8.1 ms, GC Workers: 4]
  14. [GC Worker Start (ms): Min: 27884.5, Avg: 27884.5, Max: 27884.5, Diff: 0.1]
  15. [Ext Root Scanning (ms): Min: 0.4, Avg: 0.8, Max: 1.2, Diff: 0.8, Sum: 3.1]
  16. [Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 1.4]
  17. [Processed Buffers: Min: 0, Avg: 2.8, Max: 5, Diff: 5, Sum: 11]
  18. [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
  19. [Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.6]
  20. [Object Copy (ms): Min: 4.9, Avg: 5.1, Max: 5.2, Diff: 0.3, Sum: 20.4]
  21. [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  22. [GC Worker Other (ms): Min: 0.0, Avg: 0.4, Max: 1.3, Diff: 1.3, Sum: 1.4]
  23. [GC Worker Total (ms): Min: 6.4, Avg: 6.8, Max: 7.8, Diff: 1.4, Sum: 27.2]
  24. [GC Worker End (ms): Min: 27891.0, Avg: 27891.3, Max: 27892.3, Diff: 1.3]
  25. [Code Root Fixup: 0.5 ms]
  26. [Code Root Migration: 1.3 ms]
  27. [Code Root Purge: 0.0 ms]
  28. [Clear CT: 0.2 ms]
  29. [Other: 5.8 ms]
  30. [Choose CSet: 0.0 ms]
  31. [Ref Proc: 5.0 ms]
  32. [Ref Enq: 0.1 ms]
  33. [Redirty Cards: 0.0 ms]
  34. [Free CSet: 0.2 ms]
  35. [Eden: 159.0M(159.0M)->0.0B(301.0M) Survivors: 13.0M->11.0M Heap: 328.8M(3072.0M)->167.3M(3072.0M)]
  36. Heap after GC invocations=13 (full 1):
  37. garbage-first heap total 3145728K, used 171269K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
  38. region size 1024K, 11 young (11264K), 11 survivors (11264K)
  39. Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K
  40. class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K
  41. }
  42. [Times: user=0.05 sys=0.01, real=0.02 secs]

每個過程的作用如下:

  • garbage-first heap total 3145728K, used 336645K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
    這行表示使用了G1垃圾收集器,total heap 3145728K,使用了336645K。
  • region size 1024K, 172 young (176128K), 13 survivors (13312K)
    Region大小爲1M,青年代佔用了172個(共176128K),倖存區佔用了13個(共13312K)。
  • Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K
    class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K
    java 8的新特性,去掉永久區,添加了元數據區,這塊不是本文重點,不再贅述。需要注意的是,之所以有committed和reserved,是因爲沒有設置MetaspaceSize=MaxMetaspaceSize。
  • [GC pause (G1 Evacuation Pause) (young)
    GC原因,新生代minor GC。
  • [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 1461, predicted base time: 35.25 ms, remaining time: 64.75 ms, target pause time: 100.00 ms]
    發生minor GC和full GC時,所有相關region都是要回收的。而發生併發GC時,會根據目標停頓時間動態選擇部分垃圾對並多的Region回收,這一步就是選擇Region。_pending_cards是關於RSet的Card Table。predicted base time是預測的掃描card table時間。
  • [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 159 regions, survivors: 13 regions, predicted young region time: 44.09 ms]
    這一步是添加Region到collection set,新生代一共159個Region,13個倖存區Region,這也和之前的(172 young (176128K), 13 survivors (13312K))吻合。預計收集時間是44.09 ms。
  • [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 159 regions, survivors: 13 regions, old: 0 regions, predicted pause time: 79.34 ms, target pause time: 100.00 ms]
    這一步是對上面兩步的總結。預計總收集時間79.34ms。
  • [Parallel Time: 8.1 ms, GC Workers: 4]
    由於收集過程是多線程並行(併發)進行,這裏是4個線程,總共耗時8.1ms(wall clock time)
  • [GC Worker Start (ms): Min: 27884.5, Avg: 27884.5, Max: 27884.5, Diff: 0.1]
    收集線程開始的時間,使用的是相對時間,Min是最早開始時間,Avg是平均開始時間,Max是最晚開始時間,Diff是Max-Min(此處的0.1貌似有問題)
  • [Ext Root Scanning (ms): Min: 0.4, Avg: 0.8, Max: 1.2, Diff: 0.8, Sum: 3.1]
    掃描Roots花費的時間,Sum表示total cpu time,下同。
  • [Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 1.4] [Processed Buffers: Min: 0, Avg: 2.8, Max: 5, Diff: 5, Sum: 11]
    Update RS (ms)是每個線程花費在更新Remembered Set上的時間。
  • [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
    掃描CS中的region對應的RSet,因爲RSet是points-into,所以這樣實現避免了掃描old generadion region,但是會產生float garbage。
  • [Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.6]
    掃描code root耗時。code root指的是經過JIT編譯後的代碼裏,引用了heap中的對象。引用關係保存在RSet中。
  • [Object Copy (ms): Min: 4.9, Avg: 5.1, Max: 5.2, Diff: 0.3, Sum: 20.4]
    拷貝活的對象到新region的耗時。
  • [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
    線程結束,在結束前,它會檢查其他線程是否還有未掃描完的引用,如果有,則”偷”過來,完成後再申請結束,這個時間是線程之前互相同步所花費的時間。
  • [GC Worker Other (ms): Min: 0.0, Avg: 0.4, Max: 1.3, Diff: 1.3, Sum: 1.4]
    花費在其他工作上(未列出)的時間。
  • [GC Worker Total (ms): Min: 6.4, Avg: 6.8, Max: 7.8, Diff: 1.4, Sum: 27.2]
    每個線程花費的時間和。
  • [GC Worker End (ms): Min: 27891.0, Avg: 27891.3, Max: 27892.3, Diff: 1.3]
    每個線程結束的時間。
  • [Code Root Fixup: 0.5 ms]
    用來將code root修正到正確的evacuate之後的對象位置所花費的時間。
  • [Code Root Migration: 1.3 ms]
    更新code root 引用的耗時,code root中的引用因爲對象的evacuation而需要更新。
  • [Code Root Purge: 0.0 ms]
    清除code root的耗時,code root中的引用已經失效,不再指向Region中的對象,所以需要被清除。
  • [Clear CT: 0.2 ms]
    清除card table的耗時。
  • [Other: 5.8 ms]
    [Choose CSet: 0.0 ms]
    [Ref Proc: 5.0 ms]
    [Ref Enq: 0.1 ms]
    [Redirty Cards: 0.0 ms]
    [Free CSet: 0.2 ms]
    其他事項共耗時5.8ms,其他事項包括選擇CSet,處理已用對象,引用入ReferenceQueues,釋放CSet中的region到free list。
  • [Eden: 159.0M(159.0M)->0.0B(301.0M) Survivors: 13.0M->11.0M Heap: 328.8M(3072.0M)->167.3M(3072.0M)]
    新生代清空了,下次擴容到301MB。

global concurrent marking 日誌

對於global concurrent marking過程,它的日誌如下所示:

  1. 66955.252: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 1449132032 bytes, allocation request: 579608 bytes, threshold: 1449
  2. 551430 bytes (45.00 %), source: concurrent humongous allocation]
  3. 2014-12-10T11:13:09.532+0800: 66955.252: Application time: 2.5750418 seconds
  4. 66955.259: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: requested by GC cause, GC cause: G1 Humongous Allocation]
  5. {Heap before GC invocations=1874 (full 4):
  6. garbage-first heap total 3145728K, used 1281786K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
  7. region size 1024K, 171 young (175104K), 27 survivors (27648K)
  8. Metaspace used 116681K, capacity 137645K, committed 137984K, reserved 1171456K
  9. class space used 13082K, capacity 16290K, committed 16384K, reserved 1048576K
  10. 66955.259: [G1Ergonomics (Concurrent Cycles) initiate concurrent cycle, reason: concurrent cycle initiation requested]
  11. 2014-12-10T11:13:09.539+0800: 66955.259: [GC pause (G1 Humongous Allocation) (young) (initial-mark)
  12. …….
  13. 2014-12-10T11:13:09.597+0800: 66955.317: [GC concurrent-root-region-scan-start]
  14. 2014-12-10T11:13:09.597+0800: 66955.318: Total time for which application threads were stopped: 0.0655753 seconds
  15. 2014-12-10T11:13:09.610+0800: 66955.330: Application time: 0.0127071 seconds
  16. 2014-12-10T11:13:09.614+0800: 66955.335: Total time for which application threads were stopped: 0.0043882 seconds
  17. 2014-12-10T11:13:09.625+0800: 66955.346: [GC concurrent-root-region-scan-end, 0.0281351 secs]
  18. 2014-12-10T11:13:09.625+0800: 66955.346: [GC concurrent-mark-start]
  19. 2014-12-10T11:13:09.645+0800: 66955.365: Application time: 0.0306801 seconds
  20. 2014-12-10T11:13:09.651+0800: 66955.371: Total time for which application threads were stopped: 0.0061326 seconds
  21. 2014-12-10T11:13:10.212+0800: 66955.933: [GC concurrent-mark-end, 0.5871129 secs]
  22. 2014-12-10T11:13:10.212+0800: 66955.933: Application time: 0.5613792 seconds
  23. 2014-12-10T11:13:10.215+0800: 66955.935: [GC remark 66955.936: [GC ref-proc, 0.0235275 secs], 0.0320865 secs]
  24. [Times: user=0.05 sys=0.00, real=0.03 secs]
  25. 2014-12-10T11:13:10.247+0800: 66955.968: Total time for which application threads were stopped: 0.0350098 seconds
  26. 2014-12-10T11:13:10.248+0800: 66955.968: Application time: 0.0001691 seconds
  27. 2014-12-10T11:13:10.250+0800: 66955.970: [GC cleanup 1178M->632M(3072M), 0.0060632 secs]
  28. [Times: user=0.02 sys=0.00, real=0.01 secs]
  29. 2014-12-10T11:13:10.256+0800: 66955.977: Total time for which application threads were stopped: 0.0088462 seconds
  30. 2014-12-10T11:13:10.257+0800: 66955.977: [GC concurrent-cleanup-start]
  31. 2014-12-10T11:13:10.259+0800: 66955.979: [GC concurrent-cleanup-end, 0.0024743 secs

這次發生global concurrent marking的原因是:humongous allocation,上面提過在巨大對象分配之前,會檢測到old generation 使用佔比是否超過了 initiating heap occupancy percent(45%),因爲
1449132032(used)+ 579608(allocation request:) > 1449551430(threshold),所以觸發了本次global concurrent marking。對於具體執行過程,上面的表格已經詳細講解了。值得注意的是上文中所說的initial mark往往伴隨着一次YGC,在日誌中也有體現:GC pause (G1 Humongous Allocation) (young) (initial-mark)。

後記

因爲篇幅的關係,也受限於能力水平,本文只是簡單了介紹了G1 GC的基本原理,很多細節沒有涉及到,所以說只能算是爲研究和使用它的同學打開了一扇門。一個日本人專門寫了一本書《徹底解剖「G1GC」 アルゴリズ》詳細的介紹了G1 GC,這本書也被作者放到了GitHub上,詳見參考文獻5。另外,莫樞在這方面也研究的比較多,讀者可以去高級語言虛擬機論壇向他請教,本文的很多內容也是我在此論壇上請教過後整理的。總而言之,G1是一款非常優秀的垃圾收集器,儘管還有些不完美(預測模型還不夠智能),但是希望有更多的同學來使用它,研究它,提出好的建議,讓它變的更加完善。

參考文獻

  1. Getting Started with the G1 Garbage Collector
  2. 請教G1算法的原理
  3. 關於incremental update與SATB的一點理解
  4. Tips for Tuning the Garbage First Garbage Collector
  5. g1gc-impl-book
  6. 垃圾優先型垃圾回收器調優
  7. Understanding G1 GC Logs
  8. G1: One Garbage Collector To Rule Them All
發佈了124 篇原創文章 · 獲贊 74 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章