最近遇到不少朋友過來諮詢G1調優的問題,我本身去年有專門學過一次G1,可是當時只是看了個皮毛,所以本身也有很多問題。整體來說,對於G1我有幾個疑惑,但願可以在這篇文章中獲得解決。html
在G1提出以前,經典的垃圾收集器主要有三種類型:串行收集器、並行收集器和併發標記清除收集器,這三種收集器分別能夠是知足Java應用三種不一樣的需求:內存佔用及併發開銷最小化、應用吞吐量最大化和應用GC暫停時間最小化,可是,上述三種垃圾收集器都有幾個共同的問題:(1)全部針對老年代的操做必須掃描整個老年代空間;(2)新生代和老年代是獨立的連續的內存塊,必須先決定年輕代和老年代在虛擬地址空間的位置。java
G1是一種服務端應用使用的垃圾收集器,目標是用在多核、大內存的機器上,它在大多數狀況下能夠實現指定的GC暫停時間,同時還能保持較高的吞吐量。mysql
G1適用於如下幾種應用:web
G1採起了不一樣的策略來解決並行、串行和CMS收集器的碎片、暫停時間不可控制等問題——G1將整個堆分紅相同大小的分區(Region),以下圖所示。面試
每一個分區均可能是年輕代也多是老年代,可是在同一時刻只能屬於某個代。
年輕代、倖存區、老年代這些概念還存在,成爲邏輯上的概念,這樣方便複用以前分代框架的邏輯。在物理上不須要連續,則帶來了額外的好處——有的分區內垃圾對象特別多,有的分區內垃圾對象不多,G1會優先回收垃圾對象特別多的分區,這樣能夠花費較少的時間來回收這些分區的垃圾,這也就是G1名字的由來,即首先收集垃圾最多的分區。算法
新生代其實並非適用於這種算法的,依然是在新生代滿了的時候,對整個新生代進行回收——整個新生代中的對象,要麼被回收、要麼晉升,至於新生代也採起分區機制的緣由,則是由於這樣跟老年代的策略統一,方便調整代的大小。sql
G1仍是一種帶壓縮的收集器,在回收老年代的分區時,是將存活的對象從一個分區拷貝到另外一個可用分區,這個拷貝的過程就實現了局部的壓縮。每一個分區的大小從1M到32M不等,可是都是2的冥次方。後端
一組可被回收的分區的集合。在CSet中存活的數據會在GC過程當中被移動到另外一個可用分區,CSet中的分區能夠來自Eden空間、survivor空間、或者老年代。CSet會佔用不到整個堆空間的1%大小。數據結構
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的關係。
SATB是維持併發GC的正確性的一個手段,G1GC的併發理論基礎就是SATB,SATB是由Taiichi Yuasa爲增量式標記清除垃圾收集器設計的一個標記算法。Yuasa的SATAB的標記優化主要針對標記-清除垃圾收集器的併發標記階段。按照R大的說法:CMS的incremental update設計使得它在remark階段必須從新掃描全部線程棧和整個young gen做爲root;G1的SATB設計在remark階段則只須要掃描剩下的satb_mark_queue。
SATB算法建立了一個對象圖,它是堆的一個邏輯「快照」。標記數據結構包括了兩個位圖:previous位圖和next位圖。previous位圖保存了最近一次完成的標記信息,併發標記週期會建立並更新next位圖,隨着時間的推移,previous位圖會愈來愈過期,最終在併發標記週期結束的時候,next位圖會將previous位圖覆蓋掉。
下面咱們以幾個圖例來描述SATB算法的過程:
在併發週期開始以前,NTAMS字段被設置到每一個分區當前的頂部,併發週期啓動後分配的對象會被放在TAMS以前(圖裏下邊的部分),同時被明肯定義爲隱式存活對象,而TAMS以後(圖裏上邊的部分)的對象則須要被明確地標記。
併發標記過程當中的堆分區
位於堆分區的Bottom和PTAMS之間的對象都會被標記並記錄在previous位圖中;
位於堆分區的Top和PATMS之間的對象均爲隱式存活對象,同時也記錄在previous位圖中;
在從新標記階段的最後,全部NTAMS以前的對象都會被標記
在併發標記階段分配的對象會被分配到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); } }
stab_mark_queue.enqueue方法首先嚐試將之前的值記錄在一個緩衝區中,若是這個緩衝區已經滿了,就會將當期這個SATB緩衝區「退休」並放入全局列表中,而後再給線程分配一個新的SATB緩衝區。併發標記線程會按期檢查和處理那些「被填滿」的緩衝區。
G1收集器的收集活動主要有四種操做:
第1、新生代垃圾收集的圖例以下:
G1設計了一個標記閾值,它描述的是整體Java堆大小的百分比,默認值是45,這個值能夠經過命令-XX:InitiatingHeapOccupancyPercent(IHOP)
來調整,一旦達到這個閾值就回觸發一次併發收集週期。注意:這裏的百分比是針對整個堆大小的百分比,而CMS中的CMSInitiatingOccupancyFraction
命令選型是針對老年代的百分比。併發收集週期的圖例以下:
在上圖中有幾個狀況須要注意:
第2、G1的併發標記週期包括多個階段:
併發標記週期採用的算法是咱們前文提到的SATB標記算法,產出是找出一些垃圾對象最多的老年代分區。
-XX:ConcGCThreads
來設置併發線程數,默認狀況下,G1垃圾收集器會將這個線程總數設置爲並行垃圾線程數(-XX:ParallelGCThreads
)的四分之一;併發標記會利用trace算法找到全部活着的對象,並記錄在一個bitmap中,由於在TAMS之上的對象都被視爲隱式存活,所以咱們只須要遍歷那些在TAMS之下的;記錄在標記的時候發生的引用改變,SATB的思路是在開始的時候設置一個快照,而後假定這個快照不改變,根據這個快照去進行trace,這時候若是某個對象的引用發生變化,就須要經過pre-write barrier logs將該對象的舊的值記錄在一個SATB緩衝區中,若是這個緩衝區滿了,就把它加到一個全局的列表中——G1會有併發標記的線程按期去處理這個全局列表。第3、混合收集只會回收一部分老年代分區,下圖是第一次混合收集先後的堆狀況對比。
混合收集會執行屢次,一直運行到(幾乎)全部標記點老年代分區都被回收,在這以後就會恢復到常規的新生代垃圾收集週期。當整個堆的使用率超過指定的百分比時,G1 GC會啓動新一輪的併發標記週期。在混合收集週期中,對於要回收的分區,會將該分區中存活的數據拷貝到另外一個分區,這也是爲何G1收集器最終出現碎片化的頻率比CMS收集器小得多的緣由——以這種方式回收對象,實際上伴隨着針對當前分區的壓縮。
G1收集器的模式主要有兩種:
在R大的帖子中,給出了一個假象的G1垃圾收集運行過程,以下圖所示,在結合上一小節的細節,就能夠將G1 GC的正常過程理解清楚了。
巨型對象:在G1中,若是一個對象的大小超過度區大小的一半,該對象就被定義爲巨型對象(Humongous Object)。巨型對象時直接分配到老年代分區,若是一個對象的大小超過一個分區的大小,那麼會直接在老年代分配兩個連續的分區來存放該巨型對象。巨型分區必定是連續的,分配以後也不會被移動——沒啥益處。
因爲巨型對象的存在,G1的堆中的分區就分紅了三種類型:新生代分區、老年代分區和巨型分區,以下圖所示:
若是一個巨型對象跨越兩個分區,開始的那個分區被稱爲「開始巨型」,後面的分區被稱爲「連續巨型」,這樣最後一個分區的一部分空間是被浪費掉的,若是有不少巨型對象都恰好比分區大小多一點,就會形成不少空間的浪費,從而致使堆的碎片化。若是你發現有不少因爲巨型對象分配引發的連續的併發週期,而且堆已經碎片化(明明空間夠,可是觸發了FULL GC),能夠考慮調整-XX:G1HeapRegionSize
參數,減小或消除巨型對象的分配。
關於巨型對象的回收:在JDK8u40以前,巨型對象的回收只能在併發收集週期的清除階段或FULL GC過程當中過程當中被回收,在JDK8u40(包括這個版本)以後,一旦沒有任何其餘對象引用巨型對象,那麼巨型對象也能夠在年輕代收集中被回收。
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,這說明針對老年代分區的回收速度比較慢,或者說對象過快得重新生代晉升到老年代,或者說是有不少大對象直接在老年代分配。針對上述緣由,咱們可能須要作的調整有:調大整個堆的大小、更快得觸發併發回收週期、讓更多的回收線程參與到垃圾收集的動做中。
在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的調優目標主要是在避免FULL GC和疏散失敗的前提下,儘可能實現較短的停頓時間和較高的吞吐量。關於G1 GC的調優,須要記住如下幾點:
不要本身顯式設置新生代的大小(用Xmn
或-XX:NewRatio
參數),若是顯式設置新生代的大小,會致使目標時間這個參數失效。
因爲G1收集器自身已經有一套預測和調整機制了,所以咱們首先的選擇是相信它,即調整-XX:MaxGCPauseMillis=N
參數,這也符合G1的目的——讓GC調優儘可能簡單,這裏有個取捨:若是減少這個參數的值,就意味着會調小新生代的大小,也會致使新生代GC發生得更頻繁,同時,還會致使混合收集週期中回收的老年代分區減小,從而增長FULL GC的風險。這個時間設置得越短,應用的吞吐量也會受到影響。
針對混合垃圾收集的調優。若是調整這指望的最大暫停時間這個參數仍是沒法解決問題,即在日誌中仍然能夠看到FULL GC的現象,那麼就須要本身手動作一些調整,能夠作的調整包括:
-XX:ConcGCThreads=n
這個參數,能夠增長後臺標記線程的數量,幫G1贏得這場你追我趕的遊戲;-XX:InitiatingHeapOccupancyPercent
這個參數來實現這個目標,若是將這個參數調小,G1就會更早得觸發併發垃圾收集週期。這個值須要謹慎設置:若是這個參數設置得過高,會致使FULL GC出現得頻繁;若是這個值設置得太小,又會致使G1頻繁得進行併發收集,白白浪費CPU資源。經過GC日誌能夠經過一個點來判斷GC是否正常——在一輪併發週期結束後,須要確保堆剩下的空間小於InitiatingHeapOccupancyPercent的值。-XX:G1MixedGCLiveThresholdPercent=n
這個參數表示若是一個分區中的存活對象比例超過n,就不會被挑選爲垃圾分區,所以能夠經過這個參數控制每次混合收集的分區個數,這個參數的值越大,某個分區越容易被當作是垃圾分區;(2)G1在一個併發週期中,最多經歷幾回混合收集週期,這個能夠經過-XX:G1MixedGCCountTarget=n
設置,默認是8,若是減少這個值,能夠增長每次混合收集收集的分區數,可是可能會致使停頓時間過長;(3)指望的GC停頓的最大值,由MaxGCPauseMillis
參數肯定,默認值是200ms,在混合收集週期內的停頓時間是向上規整的,若是實際運行時間比這個參數小,那麼G1就能收集更多的分區。-XX:+UseG1GC
,告訴JVM使用G1垃圾收集器-XX:MaxGCPauseMillis=200
,設置GC暫停時間的目標最大值,這是個柔性的目標,JVM會盡力達到這個目標-XX:INitiatingHeapOccupancyPercent=45
,若是整個堆的使用率超過這個值,G1會觸發一次併發週期。記住這裏針對的是整個堆空間的比例,而不是某個分代的比例。經過-Xmn
顯式設置年輕代的大小,會干擾G1收集器的默認行爲:
不要根據平均響應時間(ART)來設置-XX:MaxGCPauseMillis=n
這個參數,應該設置但願90%的GC均可以達到的暫停時間。這意味着90%的用戶請求不會超過這個響應時間,記住,這個值是一個目標,可是G1並不保證100%的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 |
Young GC、Mixed GC和Full GC的區別?
答:Young GC的CSet中只包括年輕代的分區,Mixed GC的CSet中除了包括年輕代分區,還包括老年代分區;Full GC會暫停整個引用,同時對新生代和老年代進行收集和壓縮。
ParallelGCThreads和ConcGCThreads的區別?
答:ParallelGCThreads指得是在STW階段,並行執行垃圾收集動做的線程數,ParallelGCThreads的值通常等於邏輯CPU核數,若是CPU核數大於8,則設置爲5/8 * cpus
,在SPARC等大型機上這個係數是5/16。;ConcGCThreads指的是在併發標記階段,併發執行標記的線程數,通常設置爲ParallelGCThreads的四分之一。
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大的這個帖子裏提到的:
G1裏在併發標記的時候,若是有對象的引用修改,要將舊的值寫到一個緩衝區中,這個動做先後會有一個write barrier,這段能否細說下?
答:這塊涉及到SATB標記算法的原理,SATB是指start at the beginning,即在併發收集週期的第一個階段(初始標記)是STW的,會給全部的分區作個快照,後面的掃描都是按照這個快照進行;在併發標記週期的第二個階段,併發標記,這是收集線程和應用線程同時進行的,這時候應用線程就可能修改了某些引用的值,致使上面那個快照不是完整的,所以G1就想了個辦法,我把在這個期間對對象引用的修改都記錄動做都記錄下來,有點像mysql的操做日誌。
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.
本號專一於後端技術、JVM問題排查和優化、Java面試題、我的成長和自我管理等主題,爲讀者提供一線開發者的工做和成長經驗,期待你能在這裏有所收穫。