大廠必知必會--G1收集器詳解及調優

前言

每週一次的例行發版,照常來臨。我熟悉的打開發版系統,同時也打開了服務監控系統,好讓別人看的出來我是一個對業務負責的小夥子。噸噸噸,噸噸噸,一頓操做...媽耶,這是咋回事,下游服務dubbo請求超時這麼多,看了看個人勞力土,再看了看監控,一分鐘400+的請求超時,請求超時數是原來的10倍,上升到了百的量級...java

故事發生在這週二的深夜。當時準備上一個需求,會致使服務中單個線程的IO變長,這個是預知的。在灰度中,發現了下游的服務dubbo請求超時數增長,腦海中立馬想到了去查了底層存儲的監控,發現底層存儲服務的999線都正常,憑藉着多年的經驗,腦海中一個聲音告訴我必定是服務GC出問題了。程序員

右邊的請求超時個數是以前的10倍(涉及到公司業務保密,相關監控數據打碼處理)

而後看了服務的GC,果真單次gc的時間比以前增長了很多。面試

當時立馬找了下游的服務,告訴我這部分的超時是在範圍容許以內的,內心終於鬆了一口氣。因而繼續搞其餘的業務去了,雖然我故做淡定,其實當時內心已經慌的不行了。算法

這時候機智的你是否是說不行就擴容唄,但我是那麼容易妥協的人嗎?這不是考驗我內功的時候嗎?雖然我平時幹着CRUD的活,但哪個程序員不喜歡研究各類技術呢?這是全部程序員的浪漫!因而忙完了手頭上的活以後開始了gc調優。bash

處理過程
  1. 將gc日誌打印出來。能夠在虛擬機的啓動gc參數中增長-XX:+PrintGCDetails參數,將每次的GC的耗時信息打印出來。
  2. 分析日誌,找到究竟是哪些GC階段致使了GC時間的上漲。

當時的GC日誌:併發

%xwEx[GC pause (G1 Evacuation Pause) (young), 0.0962103 secs]
   [Parallel Time: 23.3 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 146441.2, Avg: 146441.3, Max: 146441.3, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 1.5, Avg: 1.8, Max: 2.4, Diff: 1.0, Sum: 7.2]
      [Update RS (ms): Min: 1.0, Avg: 1.5, Max: 1.7, Diff: 0.6, Sum: 5.9]
         [Processed Buffers: Min: 27, Avg: 34.8, Max: 41, Diff: 14, Sum: 139]
      [Scan RS (ms): Min: 0.3, Avg: 0.3, Max: 0.3, Diff: 0.0, Sum: 1.3]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.5, Max: 1.1, Diff: 1.1, Sum: 1.9]
      [Object Copy (ms): Min: 18.3, Avg: 19.0, Max: 19.6, Diff: 1.2, Sum: 76.1]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 7]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
      [GC Worker Total (ms): Min: 23.1, Avg: 23.2, Max: 23.2, Diff: 0.1, Sum: 92.7]
      [GC Worker End (ms): Min: 146464.4, Avg: 146464.4, Max: 146464.5, Diff: 0.1]
   [Code Root Fixup: 0.1 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.7 ms]
   [Other: 72.1 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 69.9 ms]
      [Ref Enq: 0.6 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.8 ms]
   [Eden: 4824.0M(4824.0M)->0.0B(4820.0M) Survivors: 88.0M->92.0M Heap: 5044.1M(8192.0M)->224.1M(8192.0M)]
 [Times: user=0.17 sys=0.00, real=0.09 secs] 
複製代碼

當時GC中基本都是young GC類型的日誌,所以不存在Mixed GC中對於old genaration回收所形成的gc時間增長。而後發現了Ref Proc這一過程消耗的比較久,而後迅速的想到了並行開啓-XX:+ParallelRefProcEnabled,而且調大了並行標記的線程-XX:ConcGCThreads,以及增長了-XX:G1HeapRegionSize,一套下來行雲流水,開始了枯燥的發版過程。果真一切都在個人預料之中:oracle

能夠看到,單次gc的時間比以前少了一半還不止,而後看下游服務的超時率:

搞定,回去睡覺!歡迎各位點個贊或者評論留言!

等等,我這就完了?固然不是,個人目的是想讓你們看了這篇以後就知道之後如何去優化高併發服務的gc問題,我敢說即便不少在一線大廠中打拼了不少年的老開發,對gc參數調優這塊也是隻知其一;不知其二,所以這部分的知識會了,能夠很容易在面試以及在工做中脫穎而出!我會在下面的篇幅中按部就班,由淺入深的講解關於虛擬機的GC。高併發

常見的收集器對比

目前大多數的公司開發都是基於jdk8,主流使用的GC收集器主要是G1,這裏拿CMSG1進行簡單的對比,來突出G1這款收集器是多麼的優秀!可能有的小公司使用的Parallel收集器,可是Parallel真的是一款毫無特點的收集器,用它跟G1對比簡直差距太大了。這裏只先作對比,相關細節後續在詳解,這裏只是突出爲何大多數公司選擇G1的緣由。佈局

G1回收階段
  1. 初始標記
  2. 併發標記
  3. 最終標記(併發)
  4. 篩選回收(併發)
CMS回收階段
  1. 初始標記
  2. 併發標記
  3. 從新標記
  4. 併發清除
CMS和G1的相同部分
  1. 初始標記都須要停頓,即不能與用戶線程並行,須要Stop The Wrold。都是標記能與GC Roots直接關聯的對象(GC Roots的概念會在後續的虛擬機系列中詳解,本篇中與GC無關的概念先不介紹)
  2. 併發標記能夠與用戶線程一塊兒,即不須要停頓。併發標記主要是沿着GC Reference Chain進行對象可達性分析的過程。
CMS和G1的不一樣點
  1. CMS的併發回收是與用戶線程一塊兒進行的,不須要停頓。若是有垃圾是在標記過程後產生的,那麼就會產生一個回收不完全的問題。設想一下一邊打掃房間,一邊有人在那扔垃圾,那麼最後的打掃效果是否是很差?而G1的篩選回收是須要暫停用戶線程,可是是併發回收的,因此速度快,並且回收效率高。
  2. CMS的回收器不能夠對young generation回收,只能對old generation回收。所以須要與其餘收集器配合,好比ParNew或者Serial。而G1不管是young仍是old均可以回收,人家能夠獨立管理整個java堆。並且回收算法是採用的標記整理(局部是複製清除算法,由於young generation中採用複製清除效率更高),不會產生內存碎片的問題。對比CMS只是可憐的標記清除,在一些高qps場景的服務中,內存碎片化很嚴重,很容易形成Full Gc!
  3. java堆在使用這兩款回收器的時候,內存佈局不一樣。在使用CMS收集器時,新生代和老年代是物理隔離的(爲兩個不一樣的連續的內存區域)。而用G1是對整個Heap劃分紅若干個不一樣的Region,新生代和老年代是相互交叉的內存區域,是邏輯上的分類。這樣劃分的目的是規避每次在對象進行可達性分析都要在堆的全區域中進行。好比一個Collection Set中的每一個region都對應着一個Remembered set,G1能夠維護這個Remembered Set,從中挑選出最具回收性價比的region進行回收,G1嘛,就是Grabage First,提升回收效率。

所以經過對比發現,從業務的角度考慮,G1優勢不言而喻,分代收集,不容易產生內存碎片化,一次回收比較完全,不容易Full gc,併發標記,停頓時間相對可控(基於停頓時間和region佔用大小判斷是否有價值回收)。優化

G1相關概念介紹

  • Region,能夠理解爲G1所管理的堆中的一個最小單位內存。具體堆中的Region個數是按照RegionSize進行劃分,堆中的objects就是分佈在一個個Region上,若是對象大小超過了RegionSize,那麼該對象就分佈在內存區域連續的不一樣Region上。
  • Young region和Old region,顧名思義,young generation的object都是分配在young region中,old generation的大部分object都是分配在old region中(當對象的大小超過了RegionSize的一半時,這些humongous objects就會被分配在humongous region中,這部分region也會被劃分爲old generation)。young generation包括Eden-region和Survivor-region,Eden-region就是全部新生成的對象分配的region;而Survivor-region就是標記過程當中young generation存活的對象所存儲的地方,Survivor-region中某些object可能會在後續的階段被提高爲old generation。

【圖片引用自Oracle官網】 好比紅色塊的region表示Eden-region,帶有S標記的紅色塊表示Survivor-region,兩者都是屬於young generation。淺藍色的純色塊爲old region,帶有H標記的淺藍色塊表示humongous region,兩者都是屬於old generation。

那麼能夠這樣來理解整個GC過程,對於young gc而言,將整個young geenration的對象進行可達性分析,若是發現對象可達,則標記該對象是存活,若是不可達,則標記對象爲dead,而後將標記爲存活的對象拷貝到survivor regions或者old regions中,將dead objects所處的regions回收。

對於Mixed Gc而言,除了會進行上述的young GC中的對於整個young generation進行標記以及拷貝以外,還會對部分old generation中的old regions進行回收,回收的算法採用的是標記--整理,即儘量的釋放出連續的內存空間範圍。

G1回收階段分類
  1. youg-only phase:將Young regions中的對象提高到Old,存到到Old regions中,而後回收young regions。
  2. space-reclamation phase:常說的Mixed GC中的一個階段。不只對young regions進行回收,也會篩選有價值的Old regions進行回收。

這兩個階段之間是能夠轉換的,當老年代的內存使用空間與總老年代空間的百分比超過了必定的閾值(後面篇幅中會講解),不只會對young generation進行回收,也會觸發對老年代的回收。

說白了就是超過這個閾值以後,會先觸發young-only phase階段,而後young GC完了以後在進行space-reclamation phase階段,對老年代進行回收;若是老年代的使用空間佔總得老年代空間沒有到達閾值,則不會觸發space-reclamation phase,只會繼續開啓下一個youg-only phase,進行循環。

G1回收期間的停頓階段
  • youg-only phase的停頓

    1. initial Mark:標記GC Roots能直接關聯到的對象,停頓時間很短。標記以後會進入Concurrent Marking(不須要停頓),併發標記做用是肯定Old regions中的全部存活對象是否須要在接下來的space-reclamation phase保留。初始標記這一過程可能尚未跑完,另外一個不包含初始標記的young gc過程可能就已經開始幹活了。初始標記並不必定發生在全部的young gc中,只有老年代的使用空間佔總老年代空間大小超過了InitiatingHeapOccupancyPercent,纔會觸發初始標記。

    2. Remark:這一個停頓主要是爲了完成標記的過程。由於上一個過程是併發標記,是跟隨用戶線程一塊兒跑的,所以可能就會併發標記事後,一些對象隨着用戶線程的執行,可達性發生了變化,所以須要用戶線程停下來,去清理一些浮動垃圾。這一過程主要處理reference processing和一些class的卸載,雖然停頓,可是能夠有多個標記線程。

    3. CleanUp:也須要停頓。主要進行region的回收,且對空的region也會進行回收。而且肯定是否要接着進行space-reclamation phase階段。若是須要進行space-recalmation phase,則繼續進行,而後cleanUp部分young regions和old regions(有回收價值的old region)。若是肯定不須要接着進行space-reclamation phase,則當前的cleanup階段就至關因而youg-only phase的cleanUp,只是單純的表示對young generation的cleanUp已經完成了。

  • space-reclamation phase的停頓

    好比Colletions(能夠理解可能須要被回收的region的集合,而且一個GC週期中可能有多個這樣的集合)中的老年代object引用了新生代的object,那麼新生代對象所處的region就會把老年代的region記錄進新生代region維護的Remembered set中。所以space-reclamation phase階段不只會對新生代object的region進行GC,也會分析新生代region背後的Remembered set,挑選出回收性價比高的一些old regions進行回收。

【圖片來自於oracle官網】

到此爲止,我已經把我知道的G1的回收機制都告訴你了,若是你對關於G1回收的算法感興趣,好比G1標記算法Snapshot-At-The-Beginning等想進一步瞭解,能夠去oracle官網中查看,後續有時間,我也會出一期這樣的文章。接下來我會針對不一樣的GC場景進行分析,以及給出相關的建議。坐穩了,發車!

GC實戰優化環節

Full GC

這是G1(大多數收集器也一樣)的一個兜底GC的階段。緣由主要是太多old generation的內存佔用致使,好比程序中使用了某一個模板解析引擎,可是沒有提取變量,致使每一次執行都是編譯一個新的class,這種狀況就很容易致使Full GC,或者有不少humongous objects的存在。若是程序中發生了Full gc,除了GC相關的調優,也須要多花時間去優化你的業務代碼。

直白一點就是由於建立了太多的object,致使g1不能及時的去回收。常見的場景是Concurrent Marking沒有及時complete。

所以Full GC的優化思路主要分爲兩個方面:

  1. 縮小Concurrent Marking的時間;
  2. 或者調大old gen區域。

Full GC調優Tips:

  1. 若是多是大對象太多形成的,能夠gc+heap=info查看humongous regions個數。能夠增長經過-XX:G1HeapRegionSize增長Region Size,避免老年代中的大對象佔用過多的內存。

  2. 增長heap大小,對應的效果G1能夠有更多的時間去完成Concurrent Marking

  3. 增長Concurrent Marking的線程,經過-XX:ConcGCThreads設置。

  4. 強制mark階段提前進行。由於在Mark階段以前,G1會根據應用程序以前的行爲,去肯定the Initiating Heap Occupancy Percent(IHOP)閾值大小,好比是否須要執行initial Mark,以及後續CleanUp階段的space-reclamation phase;若是服務流量忽然增長或者其餘行爲改變的話,那麼基於以前的預測的閾值就會不許確,能夠採起下面的思路:

    1. 能夠增長G1在IHOP分析過程當中的所須要的內存空間,經過-XX:G1ReservePercent來設置,提升預測的效率。
    2. 關閉G1的自動IHOP分析機制,-XX:-G1UseAdaptiveIHOP,而後手動的指定這個閾值大小,-XX:InitiatingHeapOccupancyPercent。這樣就省去了每次預測的一個時間消耗。
  5. Full gc多是系統中的humongous object比較多,系統找不到一塊連續的regions區域來分配。能夠經過-XX:G1HeapRegionSize增長region size,或者將整個heap調大。

Mixed GC或者Young GC調優

Reference Object Processing時間消耗比較久

gc日誌中能夠看Ref ProcRef EnqRef ProcG1根據不一樣引用類型對象的要求去更新對應的referents;Ref EnqG1若是實際引用對象已經不可達了,那麼就會將這些引用對象加入對應的引用隊列中。若是這一過程比較長,能夠考慮將這個過程開啓並行,經過-XX:+ParallelRefProcEnabled

young-only回收較久

主要緣由是Collection Set中有太多的存活對象須要拷貝。能夠經過gc日誌中的Evacuate Collection Set看到對應的時間,能夠增長young geenration的最小大小,經過-XX:G1NewSizePercent。 也多是某一個瞬間,倖存下來的對象一會兒有不少,這種狀況會形成gc停頓時間猛漲,通常應對這種狀況經過-XX:G1MaxNewSizePercent這個參數,增長young generation最大空間。

Mixed回收時間較久

經過開啓gc+ergo+cset=trace,若是是predicated young regions花費比較長,能夠針對上文中的方法。若是是predicated old regions比較長,則能夠經過如下方法:

  • 增長-XX:G1MixedGCCountTarget這個參數,將old generation的regions分散到較多的Collection(上文有解釋)中,增長-XX:G1MixedGCCountTarget參數值。避免單次處理較大塊的Collection。

那麼如今回過頭去看我以前調整的參數,是否是明白了我調整了以後,服務的GC效率立馬提高了呢?其實調優的過程不是一蹴而就的,須要持續打磨,有了經驗以後,你看到以後想到的東西永遠比別人多!

手寫辛苦,歡迎各位點個贊留言,也順帶恭喜FPX和TES攜手共進決賽哈哈。

相關文章
相關標籤/搜索