G1 GC 全稱Garbage-First Garbage Collector 的全面全方位總結

關鍵描述

       G1是一種服務器端的垃圾收集器,應用在多處理器和大內存環境中,在實現高吞吐量的同時,儘可能的滿足垃圾收集暫停時間的要求,全堆操作(例如全局標記)與應用程序線程並行執行。這樣可以防止與堆或活動數據大小成比例的中斷。
       G1收集器的設計目標是取代CMS收集器,它同CMS相比,在以下方面表現的更出色: 
* G1是一個有整理內存過程的垃圾收集器,在回收垃圾的時候會壓縮存活對象。不會產生很多內存碎片。
* G1的Stop The World(STW)更可控,G1在停頓時間上添加了預測機制,用戶可以指定期望停頓時間。

G1中幾個重要術語

1,Region 常翻譯成分區/區域

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

       而G1也是分代管理內存的,他的各代存儲地址是不連續的,每一代都使用了n個不連續的大小相同的Region,每個Region佔有一塊連續的虛擬內存地址。Java堆空間被G1 GC分成2048個大小一樣的region,(2048個region是可以改的,使用G1 GC的jvm配置參數 -XX:G1HeapRegionSize=2M,設置單個region的大小,一個2g的堆就會被分成1024個region)   之前熟悉的內存分區名稱 Eden,Survivor和Old是n個region的邏輯集合,並不物理相鄰。
        在實際查看使用G1 GC的程序的內存使用情況的時候,使用命令jmap -heap pid 就會看到E區的regions size是x個,S區的regions size是Y個 ,O區的regions size是Z個,x+y+z < 2048,正常情況下還有好多個region是free的,未被使用的。
       還有就是這幾個分區的region個數也是一直在變化的,不像之前的其它gc,分區定了之後,各區大小很少變化。
       -XX:G1HeapRegionSize=n,設置the size of a G1 region的大小。值是 2 的冪,範圍是 1 MB 到 32 MB 之間。目標是根據最小的 Java 堆大小劃分出約 2048 個區域。看文章末尾的截圖吧。
如下圖所示:

2,巨型區域

       對於 G1 GC,任何超過區域一半大小的對象都被視爲“巨型對象”。此類對象直接被分配到老年代中的“巨型區域” Humongous。這些巨型區域是一個連續的區域集(n個地址連續的region,爲的是可以拿出來連續的地址給大對象分配空間)。StartsHumongous 標記該連續集的開始,ContinuesHumongous 標記它的延續。上圖的H區就是說的這個。H區的gc頻率就會低點兒。
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。
       如果一個巨型對象跨越兩個分區,開始的那個分區被稱爲“開始巨型”,後面的分區被稱爲“連續巨型”,這樣最後一個分區的一部分空間是被浪費掉的,如果有很多巨型對象都剛好比分區大小多一點,就會造成很多空間的浪費,從而導致堆的碎片化。如果你發現有很多由於巨型對象分配引起的連續的併發週期,並且堆已經碎片化(明明空間夠,但是觸發了FULL GC),可以考慮調整-XX:G1HeapRegionSize參數,減少或消除巨型對象的分配。
       關於巨型對象的回收:在JDK8u40之前,巨型對象的回收只能在併發收集週期的清除階段或FULL GC過程中過程中被回收,在JDK8u40(包括這個版本)之後,一旦沒有任何其他對象引用巨型對象,那麼巨型對象也可以在年輕代收集中被回收。

3,CSet (collection sets)

        一組可被回收的region的集合。在CSet中存活的數據會以增量、並行的方式複製到不同的一個或者n個新region來實現壓縮,從而減少堆碎片(類似young區的複製算法),CSet中的region可以來自Eden空間、Survivor空間、或者Old。CSet會佔用不到整個堆空間的1%大小。目標是從可回收空間最多的區域開始,儘可能回收更多的堆空間,同時儘可能不超出暫停時間目標。

4,RSet(Remembered Set)

全稱是Remembered Set,是輔助GC過程的一種結構,典型的空間換時間工具。在GC的時候,對於old->young和old->old的跨代跨region對象引用,只要掃描對應的CSet中的RSet即可。 邏輯上說每個Region都有一個RSet,RSet記錄了其他Region中的對象引用本Region中對象的關係,屬於points-into結構(誰引用了我的對象)。G1的RSet是在Card Table的基礎上實現的:每個Region會記錄下別的Region有指向自己的指針,並標記這些指針分別在哪些Card的範圍內。RSet的價值在於使得垃圾收集器不需要掃描整個堆找到誰引用了當前分區中的對象,只需要掃描RSet即可。

       如上圖所示,三個Region,每個Region被分成了多個Card,在不同Region中的Card會相互引用,Region1中的Card中的對象引用了Region2中的Card中的對象,藍色實線表示的就是points-out的關係,而在Region2的RSet中,記錄了Region1的Card,即紅色虛線表示的關係,這就是points-into。 而維繫RSet中的引用關係靠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的工作量。

5,停頓預測模型

        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數量,從而儘量滿足用戶設定的目標停頓時間。 

6,G1 GC中的幾個關鍵動作

1,young gc

    g1gc也是分代收集垃圾的,

  • Eden區耗盡的時候就會觸發新生代收集,新生代垃圾收集會對整個新生代(E + S)進行回收
  • 新生代垃圾收集期間,整個應用STW
  • 新生代垃圾收集是由多線程併發執行的
  • 通過控制年輕代的region個數,即年輕代內存大小,來控制young GC的時間開銷。
  • 新生代收集結束後依然存活的對象,會被疏散evacuation到n(n>=1)個新的Survivor分區,或者是老年代。

2,全堆併發標記

  • 初始標記(initial-mark),在此階段,G1 GC 對根進行標記。該階段與常規的 (STW) young垃圾回收密切相關。在這個階段,應用會經歷STW,通常初始標記階段會跟一次新生代收集一起進行,換句話說——既然這兩個階段都需要暫停應用,G1 GC就重用了新生代收集來完成初始標記的工作。在新生代垃圾收集中進行初始標記的工作,會讓停頓時間稍微長一點,並且會增加CPU的開銷。初始標記做的工作是設置兩個TAMS變量(NTAMS和PTAMS)的值,所有在TAMS之上的對象在這個併發週期內會被識別爲隱式存活對象;
  • 根分區掃描(root-region-scan),G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的對象。該階段與應用程序(非 STW)同時運行,並且只有完成該階段後,才能開始下一次 STW 年輕代垃圾回收。這個過程不需要暫停應用,在初始標記或新生代收集中被拷貝到survivor分區的對象,都需要被看做是根,這個階段G1開始掃描survivor分區,所有被survivor分區所引用的對象都會被掃描到並將被標記。survivor分區就是根分區,正因爲這個,該階段不能發生新生代收集,如果掃描根分區時,新生代的空間恰好用盡,新生代垃圾收集必須等待根分區掃描結束才能完成。如果在日誌中發現根分區掃描和新生代收集的日誌交替出現,就說明當前應用需要調優。
  • 併發標記階段(concurrent-mark),G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行,可以被 STW 年輕代垃圾回收中斷。併發標記階段是多線程的,我們可以通過-XX:ConcGCThreads來設置併發線程數,默認情況下,G1垃圾收集器會將這個線程總數設置爲並行垃圾線程數(-XX:ParallelGCThreads)的四分之一;併發標記會利用trace算法找到所有活着的對象,並記錄在一個bitmap中,因爲在TAMS之上的對象都被視爲隱式存活,因此我們只需要遍歷那些在TAMS之下的;記錄在標記的時候發生的引用改變,SATB的思路是在開始的時候設置一個快照,然後假定這個快照不改變,根據這個快照去進行trace,這時候如果某個對象的引用發生變化,就需要通過pre-write barrier logs將該對象的舊的值記錄在一個SATB緩衝區中,如果這個緩衝區滿了,就把它加到一個全局的列表中——G1會有併發標記的線程定期去處理這個全局列表。
  • 重新標記階段(remarking),重新標記階段是最後一個標記階段,需要暫停整個應用,G1垃圾收集器會處理掉剩下的SATB日誌緩衝區和所有更新的引用,同時G1垃圾收集器還會找出所有未被標記的存活對象。這個階段還會負責引用處理等工作。
  • 清理階段(cleanup),在這個最後階段,G1 GC 執行統計和 RSet 淨化的操作。在統計期間,G1 GC 會識別完全空閒的區域和可供進行混合垃圾回收的區域。清理階段在將空白區域重置並返回到空閒列表時爲部分併發。清理階段真正回收的內存很小,截止到這個階段,G1垃圾收集器主要是標記處哪些老年代分區可以回收,將老年代按照它們的存活度(liveness)從小到大排列。這個過程還會做幾個事情:識別出所有空閒的分區、RSet梳理、將不用的類從metaspace中卸載、回收巨型對象等等。識別出每個分區裏存活的對象有個好處是在遇到一個完全空閒的分區時,它的RSet可以立即被清理,同時這個分區可以立刻被回收並釋放到空閒隊列中,而不需要再放入CSet等待混合收集階段回收;梳理RSet有助於發現無用的引用。

        G1設計了一個標記閾值,它描述的是總體Java堆大小的百分比,默認值是45,這個值可以通過命令-XX:InitiatingHeapOccupancyPercent來調整,一旦達到這個閾值就會觸發一次併發收集週期。注意:這裏的百分比是針對整個堆大小的百分比
        在併發收集週期中,至少有一次(很可能是多次)新生代垃圾收集,上面說了併發標記和young gc一起發生,共同使用stw時間,這是因爲他們可以複用root scan操作,所以可以說global concurrent marking是伴隨Young GC而發生的;
        標記週期找出的包含最多垃圾的分區(注意:它們內部仍然保留着數據);
        老年代的空間佔用在標記週期結束後變得更多,這是因爲在標記週期期間,新生代的垃圾收集會晉升對象到老年代,而且標記週期中並不會回收老年代的任何對象。
        第四階段Cleanup只是回收了沒有存活對象的Region。

3,mixed gc  != full 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的。這個serial old GC full gc是單線程的(在Java 8中)並且非常慢,因此應避免使用。
         什麼時候發生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數量。

4,full gc 全堆gc

如果mixed GC實在無法跟上程序分配內存的速度,導致老年代填滿無法繼續進行Mixed GC,就會使用serial old GC(full GC)來收集整個GC heap。G1是不提供full GC的。這個serial old GC full gc是單線程的(在Java 8中)並且非常慢,因此應避免在G1 gc的時候出現這個full gc 。不能覺得用了G1gc收集器之後,Java heap裏面的gc不是young gc 就mixed gc,還有這個full gc呢。有必要強調一下。

7,G 1 GC支持的配置參數以及默認值

-XX:G1HeapRegionSize=n 設置the size of a G1 region的大小。值是 2 的冪,範圍是 1 MB 到 32 MB 之間。目標是根據最小的 Java 堆大小劃分出約 2048 個區域。
-XX:MaxGCPauseMillis=200 設置最長暫停時間目標值。默認值是 200 毫秒。
-XX:G1NewSizePercent=5 設置年輕代最小值所佔總堆的百分比。默認值是堆的 5%。
-XX:G1MaxNewSizePercent=60 設置年輕代最大值所佔總堆的百分比。默認值是 Java 堆的 60%。
-XX:ParallelGCThreads=n

設置 STW 並行gc工作線程數的值。將 n 的值設置爲邏輯處理器的數量。n 的值與邏輯處理器的數量相同,最多爲 8。

如果邏輯處理器不止八個,則將 n 的值設置爲邏輯處理器數的 5/8 左右。這適用於大多數情況,除非是較大的 SPARC 系統,其中 n 的值可以是邏輯處理器數的 5/16 左右。

-XX:ConcGCThreads=n 併發標記階段,併發執行的線程數。將 n 設置爲並行垃圾回收線程數 (ParallelGCThreads) 的 1/4 左右。
-XX:InitiatingHeapOccupancyPercent=45 設置觸發全局併發標記週期的 Java 堆佔用率閾值。默認佔用率是整個 Java 堆的 45%。
-XX:G1MixedGCLiveThresholdPercent=65 old generation region中的存活對象的佔比,只有佔比小於此參數的old region,纔會被選入CSet。數越大,活的對象越多,這個region可回收的就越少,gc效果就不明顯。就優先不gc這些region
-XX:G1HeapWastePercent=10

設置您願意浪費的堆百分比。如果可回收百分比小於堆廢物百分比,Java HotSpot VM 不會啓動混合垃圾週期。默認值是 10%。

在global concurrent marking結束之後,我們可以知道old gen regions中有多少空間要被回收,在每次YGC之後和再次發生Mixed GC之前,會檢查垃圾佔比是否達到此參數,只有達到了,下次纔會發生Mixed GC

-XX:G1MixedGCCountTarget=8 一次global concurrent marking之後,最多執行Mixed GC的次數。
-XX:G1OldCSetRegionThresholdPercent=10 一次Mixed GC中能被選入CSet的最多old generation region數量。默認值是 Java 堆的 10%
-XX:G1ReservePercent=10 設置作爲空閒空間的預留內存百分比,以降低目標空間溢出的風險。默認值是 10%。增加或減少百分比時,請確保對總的 Java 堆調整相同的liang

8,實際看看程序在G1 GC收集器情況下的一些信息

1,jmap -heap pid 查看G1gc收集器下的堆內存情況

可以看到整個堆的大小8g多,G1 heap region size,也就是單個region的大小是4M,總的G1 heap分成2048個region,g1 heap的容量上8192M = 8g多,New size 500M,E區分了108個regions,s區分了4個regions,Old區分了399個regions,ESO區的容量使用空閒以及使用率都有展示,還帶單位的。ESO的各個的總region的個數,是一直在變的,不同的時候,看到的個數是不一樣的。

2,jstat -gc pid  1s 10

上圖看程序的gc情況,這個程序的jvm的各種設置信息 jinfo -flags pid

和之前的gc收集器不同的是,他的s區,是隻有1個在用的,另一個一直空的,之前還說爲啥young區要2個s區呢,現在這個gc就只使用一個s區就OK了呢?這也和他分n個region的設計有關係,s區就是n個不同的region,其實已經隱式的分成了2個s區了,也可以實現copy算法的垃圾收集。上面這個截圖中young gc的頻率還是很高的,代碼裏面是幾個線程一直在消費數據。一直在產生廢對象。

因爲在jvm的設置裏面顯示的設置了young區的大小512m,然後,看到gc的頻率是刷刷刷的,一秒很多次(看YGC那列的2行的差),因爲新對象一般都會出生在young區的E區,設置了young區的大小,那麼程序一直在飛速的運轉,那麼young區是有限的,除了已經被old佔領的地方之外,還有大片的地方是free的,暫時是浪費的,然後young gc一直在幹活,爲了回收那一丟丟的空間,下面是把這個-Xmn的設置給去掉之後,的效果。

丟到young區大小的設置之後,E區就寬敞很多了,他直接2.5個多g,在new 對象的時候就寬敞多了,因爲程序裏面的對象大多都在young區使用完之後,很少部分會被晉級到old區,e區基本都是複製+整理算法在回收地盤。

下面是去掉young區大小的設置之後的gc信息

發現這個gc信息中young gc的頻率明顯降低,gc時間也少了,可以留更多的時間給cpu處理我們代碼裏面的事兒,每秒處理的數據量就稍微上來了一丟丟,下面的日誌打印也稍微能說明點問題,前面2個是之前有-Xmn設置的時候處理量,最後一個是剛剛去掉這個設置之後的處理量。

9,G1 調優

        G1的調優目標主要是在避免FULL GC和疏散失敗的前提下,儘量實現較短的停頓時間和較高的吞吐量。關於G1 GC的調優,需要記住以下幾點:
        不要顯式的設置新生代的大小(用Xmn或-XX:NewRatio參數),如果顯式設置新生代的大小,會導致目標時間這個參數失效。
        由於G1收集器自身已經有一套預測和調整機制了,因此我們首先的選擇是相信它,即調整-XX:MaxGCPauseMillis=N參數,這也符合G1的目的——讓GC調優儘量簡單,這裏有個取捨:如果減小這個參數的值,就意味着會調小新生代的大小,也會導致新生代GC發生得更頻繁,同時,還會導致混合收集週期中回收的老年代分區減少,從而增加FULL GC的風險。這個時間設置得越短,應用的吞吐量也會受到影響。
        針對混合垃圾收集的調優。如果調整這期望的最大暫停時間這個參數還是無法解決問題,即在日誌中仍然可以看到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就能收集更多的分區。

10,常見問題

1,Young GC、Mixed GC和Full GC的區別? 
        答:Young GC的CSet中只包括年輕代的region,Mixed GC的CSet中除了包括young的region,還包括n個Old的region;Full GC會暫停整個引用,同時對新生代和老年代進行收集和壓縮。
2,ParallelGCThreads和ConcGCThreads的區別? 
        答:ParallelGCThreads指得是在STW階段,並行執行垃圾收集動作的線程數,
ParallelGCThreads的值一般等於邏輯CPU核數,如果CPU核數大於8,則設置爲5/8 * cpus,在SPARC等大型機上這個係數是5/16。;ConcGCThreads指的是在併發標記階段,併發執行標記的線程數,一般設置爲ParallelGCThreads的四分之一。
3,爲什麼G1GC在較大的堆中會更好地工作?
        在開始時JVM啓動→分配了堆→堆被標記爲S,E或O→應用程序啓動→創建了對象→E空間已滿→當所有E空間都被填滿時,將發生年輕的GC→年輕GC把E區所有live對象,根據年齡和資格將它們複製到S區和O區→要進行此複製,應該有足夠的free區域(O區和S區)來容納來自young gc之後的live對象→如果堆空間小,則來自E區的對象將沒有空間可容納和壓縮。因此,G1GC適合大堆。可以理解爲複製算法和標記整理算法共同作用,複製算法就是需要大的內存。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章