1. 程式人生 > >最全面的JVM G1學習筆記

最全面的JVM G1學習筆記

引子


最近遇到很多朋友過來諮詢G1調優的問題,我自己去年有專門學過一次G1,但是當時只是看了個皮毛,因此自己也有不少問題。總體來講,對於G1我有幾個疑惑,希望能夠在這篇文章中得到解決。

  1. G1出現的初衷是什麼?

  2. G1適合在什麼場景下使用?

  3. G1的trade-off是什麼?

  4. G1的詳細過程?

  5. 如何理解G1的gc日誌?

  6. G1的調優思路?

 

一、基礎知識


1. 初衷

在G1提出之前,經典的垃圾收集器主要有三種類型:序列收集器、並行收集器和併發標記清除收集器,這三種收集器分別可以是滿足Java應用三種不同的需求:記憶體佔用及併發開銷最小化、應用吞吐量最大化和應用GC暫停時間最小化,但是,上述三種垃圾收集器都有幾個共同的問題:(1)所有針對老年代的操作必須掃描整個老年代空間;(2)年輕地和老年代是獨立的連續的記憶體塊,必須先決定年輕代和老年代在虛擬地址空間的位置。

2. 設計目標

G1是一種服務端應用使用的垃圾收集器,目標是用在多核、大記憶體的機器上,它在大多數情況下可以實現指定的GC暫停時間,同時還能保持較高的吞吐量。

3. 使用場景

G1適用於以下幾種應用:

  • 可以像CMS收集器一樣,允許垃圾收集執行緒和應用執行緒並行執行,即需要額外的CPU資源;

  • 壓縮空閒空間不會延長GC的暫停時間;

  • 需要更易預測的GC暫停時間;

  • 不需要實現很高的吞吐量

 

二、G1的重要概念


1. 分割槽(Region)

G1採取了不同的策略來解決並行、序列和CMS收集器的碎片、暫停時間不可控制等問題——G1將整個堆分成相同大小的分割槽(Region)

,如下圖所示。

每個分割槽都可能是年輕代也可能是老年代,但是在同一時刻只能屬於某個代。 年輕代、倖存區、老年代這些概念還存在,成為邏輯上的概念,這樣方便複用之前分代框架的邏輯。在物理上不需要連續,則帶來了額外的好處——有的分割槽內垃圾物件特別多,有的分割槽內垃圾物件很少,G1會優先回收垃圾物件特別多的分割槽,這樣可以花費較少的時間來回收這些分割槽的垃圾,這也就是G1名字的由來,即首先收集垃圾最多的分割槽。

新生代其實並不是適用於這種演算法的,依然是在新生代滿了的時候,對整個新生代進行回收—— 整個新生代中的物件,要麼被回收、要麼晉升,至於新生代也採取分割槽機制的原因,則是因為這樣跟老年代的策略統一,方便調整代的大小。

G1還是一種帶壓縮的收集器,在回收老年代的分割槽時,是將存活的物件從一個分割槽拷貝到另一個可用分割槽,這個拷貝的過程就實現了局部的壓縮。每個分割槽的大小從1M到32M不等,但是都是2的冥次方。

2. 收集集合(CSet)

一組可被回收的分割槽的集合。在CSet中存活的資料會在GC過程中被移動到另一個可用分割槽,CSet中的分割槽可以來自Eden空間、survivor空間、或者老年代。CSet會佔用不到整個堆空間的1%大小。

3. 已記憶集合(RSet)

RSet記錄了其他Region中的物件引用本Region中物件的關係,屬於points-into結構(誰引用了我的物件)。RSet的價值在於使得垃圾收集器不需要掃描整個堆找到誰引用了當前分割槽中的物件,只需要掃描RSet即可。

如下圖所示,Region1和Region3中的物件都引用了Region2中的物件,因此在Region2的RSet中記錄了這兩個引用。

摘一段R大的解釋:G1 GC則是在points-out的card table之上再加了一層結構來構成points-into RSet:每個region會記錄下到底哪些別的region有指向自己的指標,而這些指標分別在哪些card的範圍內。 這個RSet其實是一個hash table,key是別的region的起始地址,value是一個集合,裡面的元素是card table的index。 舉例來說,如果region A的RSet裡有一項的key是region B,value裡有index為1234的card,它的意思就是region B的一個card裡有引用指向region A。所以對region A來說,該RSet記錄的是points-into的關係;而card table仍然記錄了points-out的關係。

4. Snapshot-At-The-Beginning(SATB)

SATB是維持併發GC的正確性的一個手段,G1GC的併發理論基礎就是SATB,SATB是由Taiichi Yuasa為增量式標記清除垃圾收集器設計的一個標記演算法。Yuasa的SATAB的標記優化主要針對標記-清除垃圾收集器的併發標記階段。按照R大的說法:CMS的incremental update設計使得它在remark階段必須重新掃描所有執行緒棧和整個young gen作為root;G1的SATB設計在remark階段則只需要掃描剩下的satbmarkqueue。

SATB演算法建立了一個物件圖,它是堆的一個邏輯“快照”。標記資料結構包括了兩個點陣圖:previous點陣圖和next點陣圖。previous點陣圖儲存了最近一次完成的標記資訊,併發標記週期會建立並更新next點陣圖,隨著時間的推移,previous點陣圖會越來越過時,最終在併發標記週期結束的時候,next點陣圖會將previous點陣圖覆蓋掉。 下面我們以幾個圖例來描述SATB演算法的過程:

  1. 在併發週期開始之前,NTAMS欄位被設定到每個分割槽當前的頂部,併發週期啟動後分配的物件會被放在TAMS之前(圖裡下邊的部分),同時被明確定義為隱式存活物件,而TAMS之後(圖裡上邊的部分)的物件則需要被明確地標記。

     

  2. 併發標記過程中的堆分割槽

     

  3. 位於堆分割槽的Bottom和PTAMS之間的物件都會被標記並記錄在previous點陣圖中;

  4. 位於堆分割槽的Top和PATMS之間的物件均為隱式存活物件,同時也記錄在previous點陣圖中;

     

  5. 在重新標記階段的最後,所有NTAMS之前的物件都會被標記

     

  6. 在併發標記階段分配的物件會被分配到NTAMS之後的空間,它們會作為隱式存活物件被記錄在next點陣圖中。一次併發標記週期完成後,這個next點陣圖會覆蓋previous點陣圖,然後將next點陣圖清空。

SATB是一個快照標記演算法,在併發標記進行的過程中,垃圾收集器(Collecotr)和應用程式(Mutator)都在活動,如果一個物件還沒被mark到,這時候Mutator就修改了它的引用,那麼這時候拿到的快照就是不完整的了,如何解決這個問題呢?G1 GC使用了SATB write barrier來解決這個問題——在併發標記過程中,將該物件的舊的引用記錄在一個SATB日誌對列或緩衝區中。去翻G1的程式碼,卻發現實際程式碼如下——只該物件入佇列,並沒有將整個修改過程放在寫屏障之間完成。

 // hotspot/src/share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.hpp
 // This notes that we don't need to access any BarrierSet data
 // structures, so this can be called from a static context.
 template <class T> static void write_ref_field_pre_static(T* field, oop newVal) {
   T heap_oop = oopDesc::load_heap_oop(field);
   if (!oopDesc::is_null(heap_oop)) {
     enqueue(oopDesc::decode_heap_oop(heap_oop));
   }
 }

enqueue的真正程式碼在 hotspot/src/share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.cpp中,這裡使用 JavaThread::satb_mark_queue_set().is_active()判斷是否處於併發標記週期。

void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
 // Nulls should have been already filtered.
 assert(pre_val->is_oop(true), "Error");

 if (!JavaThread::satb_mark_queue_set().is_active()) return;
 Thread* thr = Thread::current();
 if (thr->is_Java_thread()) {
   JavaThread* jt = (JavaThread*)thr;
   //將舊值入隊
   jt->satb_mark_queue().enqueue(pre_val);
 } else {
   MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag);
   JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
 }
}

stabmarkqueue.enqueue方法首先嚐試將以前的值記錄在一個緩衝區中,如果這個緩衝區已經滿了,就會將當期這個SATB緩衝區“退休”並放入全域性列表中,然後再給執行緒分配一個新的SATB緩衝區。併發標記執行緒會定期檢查和處理那些“被填滿”的緩衝區。

 

三、G1的過程


1. 四個操作

G1收集器的收集活動主要有四種操作:

  • 新生代垃圾收集

  • 後臺收集、併發週期

  • 混合式垃圾收集

  • 必要時候的Full GC

第一、新生代垃圾收集的圖例如下:

  • Eden區耗盡的時候就會觸發新生代收集,新生代垃圾收集會對整個新生代進行回收

  • 新生代垃圾收集期間,整個應用STW

  • 新生代垃圾收集是由多執行緒併發執行的

  • 新生代收集結束後依然存活的物件,會被拷貝到一個新的Survivor分割槽,或者是老年代。

G1設計了一個標記閾值,它描述的是總體Java堆大小的百分比,預設值是45,這個值可以通過命令 -XX:InitiatingHeapOccupancyPercent(IHOP)來調整,一旦達到這個閾值就回觸發一次併發收集週期。注意:這裡的百分比是針對整個堆大小的百分比,而CMS中的 CMSInitiatingOccupancyFraction命令選型是針對老年代的百分比。併發收集週期的圖例如下:

在上圖中有幾個情況需要注意:

  • 新生代的空間佔用情況發生了變化——在併發收集週期中,至少有一次(很可能是多次)新生代垃圾收集;

  • 注意到一些分割槽被標記為X,這些分割槽屬於老年代,它們就是標記週期找出的包含最多垃圾的分割槽(注意:它們內部仍然保留著資料);

  • 老年代的空間佔用在標記週期結束後變得更多,這是因為在標記週期期間,新生代的垃圾收集會晉升物件到老年代,而且標記週期中並不會是否老年代的任何物件。

第二、G1的併發標記週期包括多個階段: 併發標記週期採用的演算法是我們前文提到的SATB標記演算法,產出是找出一些垃圾物件最多的老年代分割槽。

  • 初始標記(initial-mark),在這個階段,應用會經歷STW,通常初始標記階段會跟一次新生代收集一起進行,換句話說——既然這兩個階段都需要暫停應用,G1 GC就重用了新生代收集來完成初始標記的工作。在新生代垃圾收集中進行初始標記的工作,會讓停頓時間稍微長一點,並且會增加CPU的開銷。初始標記做的工作是設定兩個TAMS變數(NTAMS和PTAMS)的值,所有在TAMS之上的物件在這個併發週期內會被識別為隱式存活物件;

  • 根分割槽掃描(root-region-scan),這個過程不需要暫停應用,在初始標記或新生代收集中被拷貝到survivor分割槽的物件,都需要被看做是根,這個階段G1開始掃描survivor分割槽,所有被survivor分割槽所引用的物件都會被掃描到並將被標記。survivor分割槽就是根分割槽,正因為這個,該階段不能發生新生代收集,如果掃描根分割槽時,新生代的空間恰好用盡,新生代垃圾收集必須等待根分割槽掃描結束才能完成。如果在日誌中發現根分割槽掃描和新生代收集的日誌交替出現,就說明當前應用需要調優。

  • 併發標記階段(concurrent-mark),併發標記階段是多執行緒的,我們可以通過 -XX:ConcGCThreads來設定併發執行緒數,預設情況下,G1垃圾收集器會將這個執行緒總數設定為並行垃圾執行緒數( -XX:ParallelGCThreads)的四分之一;併發標記會利用trace演算法找到所有活著的物件,並記錄在一個bitmap中,因為在TAMS之上的物件都被視為隱式存活,因此我們只需要遍歷那些在TAMS之下的;記錄在標記的時候發生的引用改變,SATB的思路是在開始的時候設定一個快照,然後假定這個快照不改變,根據這個快照去進行trace,這時候如果某個物件的引用發生變化,就需要通過pre-write barrier logs將該物件的舊的值記錄在一個SATB緩衝區中,如果這個緩衝區滿了,就把它加到一個全域性的列表中——G1會有併發標記的執行緒定期去處理這個全域性列表。

  • 重新標記階段(remarking),重新標記階段是最後一個標記階段,需要暫停整個應用,G1垃圾收集器會處理掉剩下的SATB日誌緩衝區和所有更新的引用,同時G1垃圾收集器還會找出所有未被標記的存活物件。這個階段還會負責引用處理等工作。

  • 清理階段(cleanup),清理階段真正回收的記憶體很小,截止到這個階段,G1垃圾收集器主要是標記處哪些老年代分割槽可以回收,將老年代按照它們的存活度(liveness)從小到大排列。這個過程還會做幾個事情:識別出所有空閒的分割槽、RSet梳理、將不用的類從metaspace中解除安裝、回收巨型物件等等。識別出每個分割槽裡存活的物件有個好處是在遇到一個完全空閒的分割槽時,它的RSet可以立即被清理,同時這個分割槽可以立刻被回收並釋放到空閒佇列中,而不需要再放入CSet等待混合收集階段回收;梳理RSet有助於發現無用的引用。

第三、混合收集只會回收一部分老年代分割槽,下圖是第一次混合收集前後的堆情況對比。

混合收集會執行多次,一直執行到(幾乎)所有標記點老年代分割槽都被回收,在這之後就會恢復到常規的新生代垃圾收集週期。當整個堆的使用率超過指定的百分比時,G1 GC會啟動新一輪的併發標記週期。在混合收集週期中,對於要回收的分割槽,會將該分割槽中存活的資料拷貝到另一個分割槽,這也是為什麼G1收集器最終出現碎片化的頻率比CMS收集器小得多的原因——以這種方式回收物件,實際上伴隨著針對當前分割槽的壓縮。

2. 兩個模式

G1收集器的模式主要有兩種:

  • Young GC(新生代垃圾收集)

  • Mixed GC(混合垃圾收集)

在R大的帖子中,給出了一個假象的G1垃圾收集執行過程,如下圖所示,在結合上一小節的細節,就可以將G1 GC的正常過程理解清楚了。

3. 巨型物件的管理

巨型物件:在G1中,如果一個物件的大小超過分割槽大小的一半,該物件就被定義為巨型物件(Humongous Object)。巨型物件時直接分配到老年代分割槽,如果一個物件的大小超過一個分割槽的大小,那麼會直接在老年代分配兩個連續的分割槽來存放該巨型物件。巨型分割槽一定是連續的,分配之後也不會被移動——沒啥益處。

由於巨型物件的存在,G1的堆中的分割槽就分成了三種類型:新生代分割槽、老年代分割槽和巨型分割槽,如下圖所示:

如果一個巨型物件跨越兩個分割槽,開始的那個分割槽被稱為“開始巨型”,後面的分割槽被稱為“連續巨型”,這樣最後一個分割槽的一部分空間是被浪費掉的,如果有很多巨型物件都剛好比分割槽大小多一點,就會造成很多空間的浪費,從而導致堆的碎片化。如果你發現有很多由於巨型物件分配引起的連續的併發週期,並且堆已經碎片化(明明空間夠,但是觸發了FULL GC),可以考慮調整 -XX:G1HeapRegionSize引數,減少或消除巨型物件的分配。

關於巨型物件的回收:在JDK8u40之前,巨型物件的回收只能在併發收集週期的清除階段或FULL GC過程中過程中被回收,在JDK8u40(包括這個版本)之後,一旦沒有任何其他物件引用巨型物件,那麼巨型物件也可以在年輕代收集中被回收。

4. G1執行過程中的異常情況

併發標記週期開始後的FULL GC

G1啟動了標記週期,但是在併發標記完成之前,就發生了Full GC,日誌常常如下所示:

51.408: [GC concurrent-mark-start]
65.473: [Full GC 4095M->1395M(4096M), 6.1963770 secs]
[Times: user=7.87 sys=0.00, real=6.20 secs]
71.669: [GC concurrent-mark-abort]

GC concurrent-mark-start開始之後就發生了FULL GC,這說明針對老年代分割槽的回收速度比較慢,或者說物件過快得從新生代晉升到老年代,或者說是有很多大物件直接在老年代分配。針對上述原因,我們可能需要做的調整有:調大整個堆的大小、更快得觸發併發回收週期、讓更多的回收執行緒參與到垃圾收集的動作中。

混合收集模式中的FULL GC

在GC日誌中觀察到,在一次混合收集之後跟著一條FULL GC,這意味著混合收集的速度太慢,在老年代釋放出足夠多的分割槽之前,應用程式就來請求比當前剩餘可分配空間大的記憶體。針對這種情況我們可以做的調整:增加每次混合收集收集掉的老年代分割槽個數;增加併發標記的執行緒數;提高混合收集發生的頻率。

疏散失敗(轉移失敗)

在新生代垃圾收集快結束時,找不到可用的分割槽接收存活下來的物件,常見如下的日誌:

60.238: [GC pause (young) (to-space overflow), 0.41546900 secs]

這意味著整個堆的碎片化已經非常嚴重了,我們可以從以下幾個方面調整:(1)增加整個堆的大小——通過增加 -XX:G1ReservePercent選項的值(並相應增加總的堆大小),為“目標空間”增加預留記憶體量;(2)通過減少 -XX:InitiatingHeapOccupancyPercent提前啟動標記週期;(3) 你也可以通過增加 -XX:ConcGCThreads選項的值來增加併發標記執行緒的數目;

巨型物件分配失敗

如果在GC日誌中看到莫名其妙的FULL GC日誌,又對應不到上述講過的幾種情況,那麼就可以懷疑是巨型物件分配導致的,這裡我們可以考慮使用 jmap命令進行堆dump,然後通過MAT對堆轉儲檔案進行分析。關於堆轉儲檔案的分析技巧,後續會有專門的文章介紹。

 

四、G1的調優


G1的調優目標主要是在避免FULL GC和疏散失敗的前提下,儘量實現較短的停頓時間和較高的吞吐量。關於G1 GC的調優,需要記住以下幾點:

  1. 不要自己顯式設定新生代的大小(用 Xmn-XX:NewRatio引數),如果顯式設定新生代的大小,會導致目標時間這個引數失效。

  2. 由於G1收集器自身已經有一套預測和調整機制了,因此我們首先的選擇是相信它,即調整 -XX:MaxGCPauseMillis=N引數,這也符合G1的目的——讓GC調優儘量簡單,這裡有個取捨:如果減小這個引數的值,就意味著會調小新生代的大小,也會導致新生代GC發生得更頻繁,同時,還會導致混合收集週期中回收的老年代分割槽減少,從而增加FULL GC的風險。這個時間設定得越短,應用的吞吐量也會受到影響。

  3. 針對混合垃圾收集的調優。如果調整這期望的最大暫停時間這個引數還是無法解決問題,即在日誌中仍然可以看到FULL GC的現象,那麼就需要自己手動做一些調整,可以做的調整包括:

    • 調整G1垃圾收集的後臺執行緒數,通過設定 -XX:ConcGCThreads=n這個引數,可以增加後臺標記執行緒的數量,幫G1贏得這場你追我趕的遊戲;

    • 調整G1垃圾收集器併發週期的頻率,如果讓G1更早得啟動垃圾收集,也可以幫助G1贏得這場比賽,那麼可以通過設定 -XX:InitiatingHeapOccupancyPercent這個引數來實現這個目標,如果將這個引數調小,G1就會更早得觸發併發垃圾收集週期。這個值需要謹慎設定:如果這個引數設定得太高,會導致FULL GC出現得頻繁;如果這個值設定得過小,又會導致G1頻繁得進行併發收集,白白浪費CPU資源。通過GC日誌可以通過一個點來判斷GC是否正常——在一輪併發週期結束後,需要確保堆剩下的空間小於InitiatingHeapOccupancyPercent的值。

    • 調整G1垃圾收集器的混合收集的工作量,即在一次混合垃圾收集中儘量多處理一些分割槽,可以從另外一方面提高混合垃圾收集的頻率。在一次混合收集中可以回收多少分割槽,取決於三個因素:(1)有多少個分割槽被認定為垃圾分割槽, -XX:G1MixedGCLiveThresholdPercent=n這個引數表示如果一個分割槽中的存活物件比例超過n,就不會被挑選為垃圾分割槽,因此可以通過這個引數控制每次混合收集的分割槽個數,這個引數的值越大,某個分割槽越容易被當做是垃圾分割槽;(2)G1在一個併發週期中,最多經歷幾次混合收集週期,這個可以通過 -XX:G1MixedGCCountTarget=n設定,預設是8,如果減小這個值,可以增加每次混合收集收集的分割槽數,但是可能會導致停頓時間過長;(3)期望的GC停頓的最大值,由 MaxGCPauseMillis引數確定,預設值是200ms,在混合收集週期內的停頓時間是向上規整的,如果實際執行時間比這個引數小,那麼G1就能收集更多的分割槽。

 

五、G1的最佳實踐


1. 關鍵引數項

  • -XX:+UseG1GC,告訴JVM使用G1垃圾收集器

  • -XX:MaxGCPauseMillis=200,設定GC暫停時間的目標最大值,這是個柔性的目標,JVM會盡力達到這個目標

  • -XX:INitiatingHeapOccupancyPercent=45,如果整個堆的使用率超過這個值,G1會觸發一次併發週期。記住這裡針對的是整個堆空間的比例,而不是某個分代的比例。

2. 最佳實踐

不要設定年輕代的大小

通過 -Xmn顯式設定年輕代的大小,會干擾G1收集器的預設行為:

  • G1不再以設定的暫停時間為目標,換句話說,如果設定了年輕代的大小,就無法實現自適應的調整來達到指定的暫停時間這個目標

  • G1不能按需擴大或縮小年輕代的大小

響應時間度量

不要根據平均響應時間(ART)來設定 -XX:MaxGCPauseMillis=n這個引數,應該設定希望90%的GC都可以達到的暫停時間。這意味著90%的使用者請求不會超過這個響應時間,記住,這個值是一個目標,但是G1並不保證100%的GC暫停時間都可以達到這個目標

3. G1 GC的引數選項

引數名 含義 預設值
-XX:+UseG1GC 使用G1收集器 JDK1.8中還需要顯式指定
-XX:MaxGCPauseMillis=n 設定一個期望的最大GC暫停時間,這是一個柔性的目標,JVM會盡力去達到這個目標 200
-XX:InitiatingHeapOccupancyPercent=n 當整個堆的空間使用百分比超過這個值時,就會觸發一次併發收集週期,記住是整個堆 45
-XX:NewRatio=n 新生代和老年代的比例 2
-XX:SurvivorRatio=n Eden空間和Survivor空間的比例 8
-XX:MaxTenuringThreshold=n 物件在新生代中經歷的最多的新生代收集,或者說最大的歲數 G1中是15
-XX:ParallelGCThreads=n 設定垃圾收集器的並行階段的垃圾收集執行緒數 不同的平臺有不同的值
-XX:ConcGCThreads=n 設定垃圾收集器併發執行GC的執行緒數 n一般是ParallelGCThreads的四分之一
-XX:G1ReservePercent=n 設定作為空閒空間的預留記憶體百分比,以降低目標空間溢位(疏散失敗)的風險。預設值是 10%。增加或減少這個值,請確保對總的 Java 堆調整相同的量 10
-XX:G1HeapRegionSize=n 分割槽的大小 堆記憶體大小的1/2000,單位是MB,值是2的冪,範圍是1MB到32MB之間
-XX:G1HeapWastePercent=n 設定您願意浪費的堆百分比。如果可回收百分比小於堆廢物百分比,JavaHotSpotVM不會啟動混合垃圾回收週期(注意,這個引數可以用於調整混合收集的頻率)。 JDK1.8是5
-XX:G1MixedGCCountTarget=8 設定併發週期後需要執行多少次混合收集,如果混合收集中STW的時間過長,可以考慮增大這個引數。(注意:這個可以用來調整每次混合收集中回收掉老年代分割槽的多少,即調節混合收集的停頓時間) 8
-XX:G1MixedGCLiveThresholdPercent=n 一個分割槽是否會被放入mix GC的CSet的閾值。對於一個分割槽來說,它的存活物件率如果超過這個比例,則改分割槽不會被列入mixed gc的CSet中 JDK1.6和1.7是65,JDK1.8是85

常見問題

  1. Young GC、Mixed GC和Full GC的區別? 答:Young GC的CSet中只包括年輕代的分割槽,Mixed GC的CSet中除了包括年輕代分割槽,還包括老年代分割槽;Full GC會暫停整個引用,同時對新生代和老年代進行收集和壓縮。

  2. ParallelGCThreads和ConcGCThreads的區別? 答:ParallelGCThreads指得是在STW階段,並行執行垃圾收集動作的執行緒數,ParallelGCThreads的值一般等於邏輯CPU核數,如果CPU核數大於8,則設定為 5/8*cpus,在SPARC等大型機上這個係數是5/16。;ConcGCThreads指的是在併發標記階段,併發執行標記的執行緒數,一般設定為ParallelGCThreads的四分之一。

  3. write barrier在GC中的作用?如何理解G1 GC中write barrier的作用? 寫屏障是一種記憶體管理機制,用在這樣的場景——當代碼嘗試修改一個物件的引用時,在前面放上寫屏障就意味著將這個物件放在了寫屏障後面。write barrier在GC中的作用有點複雜,我們這裡以trace GC演算法為例講下:trace GC有些演算法是併發的,例如CMS和G1,即使用者執行緒和垃圾收集執行緒可以同時執行,即mutator一邊跑,collector一邊收集。這裡有一個限制是:黑色的物件不應該指向任何白色的物件。如果mutator檢視讓一個黑色的物件指向一個白色的物件,這個限制就會被打破,然後GC就會失敗。針對這個問題有兩種解決思路:(1)通過新增read barriers阻止mutator看到白色的物件;(2)通過write barrier阻止mutator修改一個黑色的物件,讓它指向一個白色的物件。write barrier的解決方法就是講黑色的物件放到寫write barrier後面。如果真得發生了white-on-black這種寫需求,一般也有多種修正方法:增量得將白色的物件變灰,將黑色的物件重新置灰等等。我理解,增量的變灰就是CMS和G1裡併發標記的過程,將黑色的物件重新變灰就是利用卡表或SATB的緩衝區將黑色的物件重新置灰的過程,當然會在重新標記中將所有灰色的物件處理掉。關於G1中write barrier的作用,可以參考R大的這個帖子裡提到的:

     

  4. G1裡在併發標記的時候,如果有物件的引用修改,要將舊的值寫到一個緩衝區中,這個動作前後會有一個write barrier,這段可否細說下? 答:這塊涉及到SATB標記演算法的原理,SATB是指start at the beginning,即在併發收集週期的第一個階段(初始標記)是STW的,會給所有的分割槽做個快照,後面的掃描都是按照這個快照進行;在併發標記週期的第二個階段,併發標記,這是收集執行緒和應用執行緒同時進行的,這時候應用執行緒就可能修改了某些引用的值,導致上面那個快照不是完整的,因此G1就想了個辦法,我把在這個期間對物件引用的修改都記錄動作都記錄下來,有點像mysql的操作日誌。

  5. GC演算法中的三色標記演算法怎麼理解? trace GC將物件分為三類:白色(垃圾收集器未探測到的物件)、灰色(活著的物件,但是依然沒有被垃圾收集器掃描過)、黑色(活著的物件,並且已經被垃圾收集器掃描過)。垃圾收集器的工作過程,就是通過灰色物件的指標掃描它指向的白色物件,如果找到一個白色物件,就將它設定為灰色,如果某個灰色物件的可達物件已經全部找完,就將它設定為黑色物件。當在當前集合中找不到灰色的物件時,就說明該集合的回收動作完成,然後所有白色的物件的都會被回收。PS:這個問題來自參考資料17,我將原文也貼在下面: > For a tracing collector (marking or copying), one conceptually colours the data white (not yet seen by the collector), black (alive and scanned by the collector) and grey (alive but not yet scanned by the collector). The collector proceeds by scanning grey objects for pointers to white objects. The white objects found are turned grey, and the grey objects scanned are turned black. When there are no more grey objects, the collection is complete and all the white objects can be recycled.