G1理論基礎與Full GC、內存抖動經驗分享

1. G1理論基礎

​ 關於G1收集器的收集過程,看過一些博客和書,基本上十有八九都說的不同,若是要肯定哪一個是正確的仍是得去看實現代碼。固然我不打算去學C語言看代碼了,接下來就結合本身的理解和資料來介紹G1收集器收集過程,力求作到初次接觸G1也能看明白。php

1.1. G1介紹

​ G1能夠說是個里程碑式的產品,從提出到正式商業使用幾乎用了差很少10年,從jdk9開始默認使用的垃圾收集器就是G1,日後會逐步取代CMS。G1與以前的垃圾收集器相比有着明顯的區別。好比它能夠進行新時代和老年代的收集,而其餘的收集器都是新生代收集器 + 老年代收集器的組合;同時對堆的劃分與傳統的堆劃分也有着明顯的區別;算法

​ 若是忽略掉永久代的話,能夠將堆劃分爲新生代的1個Eden + 2 個Survivor以及一大塊老年代而後進行分代收集,以下圖所示:shell

​ 再看G1收集器,則是將整個大堆劃分爲不少大小相同的region,region大小爲2的n次冪,並且不要求region間是空間上連續的。每一個Region則能夠扮演eden、survivor、old三種角色之一。其實還有humonous區,其專門用來存儲大對象,大對象定義爲大小達到region一半大小的對象,由於G1會把它當作老年代,因此這裏放到老年代一塊兒了。另外每一個region扮演的角色不是一直不變的。數組

​ 到這就能夠解釋爲何取名叫G1收集器了:收集器根據用戶指定的停頓時間來制定回收計劃,簡單來講就是,對各個區域的回收價值排序收集,好比有的region中對象存活的極少,那麼該region確定排的很是靠前而優先被收集。由於收集這塊region只須要移動不多的對象,那麼就只須要不多的時間,同時還能釋放大量的內存,必然優先收集該區域。這就是G1(Garbage First)名字的由來。瀏覽器

​ G1只有YongGC(YGC)和mixed gc,YGC用於收集所有的新生代,而mixed gc則是收集所有的年輕代和收益高的老年代。當發生Full GC則會使用單線程進行GC,很是耗時,應該極力去避免Full GC的發生。下面經過介紹G1的回收過程來闡述使用G1的相比其餘收集器的優點。bash

1.2. YongGC

​ 具體的GC過程不會說的太細,不然得寫幾萬字,由於還要介紹記憶集,卡表等。數據結構

​ 收集後以下:多線程

​ 如圖所示,年輕代被挑選出來後,收集的時候會將存活對象移動到一塊空的region中去,而後再一次性清理原來的內存空間釋放內存,須要強調的是YGC針對的是整個新生代,不少狀況下原來Eden區域有個1-2G,一次YGC後所有清零,而survivor區域內存佔用會上漲一些,畢竟絕大部分對象都是朝生夕死,因此YGC效率仍是很高的。如下有幾點須要強調:併發

  1. 基本上Eden區域所有清零,1666M -> 0;
  2. 新生代存活的對象都會移動到一塊或者多塊空的region區,以後將這塊區域標記爲survivor區;可是survivor中的對象若是達到年齡後則會進入到老年代, 若是空間不足,eden區域內存活對象也是會進入到老年代的;
  3. 大對象會進入到humogous,收集器會把它當作老年代來看;
  4. YGC會stop the world(STW),可是通常來講因爲YGC效率高(同時使用多線程同時收集),因此STW的時間很短。

1.3. mixed gc

​ mixed gc(混合收集)是G1獨有的gc方式,mixed gc會收集所有新生代和收益高的老年代。mixed gc主要分爲兩步:app

  • 全局併發標記( global concurrent marking),爲mixed gc提供標記服務;
  • 混合垃圾回收,包含拷貝存活對象並清理內存;

​ 而全局併發標記又分爲四步:

  1. 初始標記(包含了根區域掃描);

  2. 併發標記;

  3. 從新標記;

  4. 清理;

    初始標記階段: 標記了從GC Roots開始直接可達的根對象, 須要STW。混合收集中的初始標記和新生代的初始標記幾乎同樣,因此常常會看到文章說與YGC的時候同步完成的,因此G1收集器在這個階段實際並無額外的停頓。實際上混合收集的初始標記是借用了新生代收集的結果,即新生代垃圾回收後的新生代Survivor分區做爲根,因此混合收集必定發生在新生代回收以後,且不須要再進行一次初始標記。在gc日誌中表現爲GC pause (young)(inital-mark).

    併發標記階段:當YGC執行結束以後,若是發現知足併發標記的條件,併發線程就開始進行併發標記。根據新生代的Survivor分區以及老生代的RSet開始併發標記。時機是在YGC後,只有達到InitiatingHeapOccupancyPercent閾值後,纔會觸發併發標記。InitiatingHeapOccupancyPercent默認值是45,表示的是當已經分配的內存加上即將分配的內存超過內存總容量的45%就能夠開始併發標記。併發標記會對全部的分區進行標記。這個階段並不須要STW。

    ​ 小tips:若是你發現收集完後佔用的堆內存達到了總的堆內存的40%之後,下次可能就會發生mixed gc了,若是發現堆內存2G,下次成了1.3G,極可能就是發生了mixed gc,頻繁的mixed gc那確定是有問題的。關於如何查看日誌中有效信息這會在後面具體介紹。

    從新標記階段:用來標記那些在併發標記階段引用發生變化的對象,須要對用戶線程作個短暫的STW。

    清理階段:在該階段也是須要STW的。主要統計存活對象,統計的結果將會用來排序分區region,以用於下一次的Collect Set(簡稱CSet,見該節最後的介紹)的選擇;須要把新分配的對象,即不在本次併發標記範圍內的新分配對象,都視爲活躍對象。

    ​ 該階段比較容易引發誤解地方在於,清理操做並不會清理垃圾對象,也不會執行存活對象的拷貝。也就是說,在極端狀況下,該階段結束以後,JVM的內存使用狀況毫無變化。

混合垃圾回收階段: 混合回收實際上與YGC是同樣的, 第一個步驟是從分區中選出若干個分區進行回收,這些被選中的分區稱爲Collect Set(簡稱CSet);第二個步驟是把這些分區中存活的對象複製到空閒的分區中去,同時把這些已經被回收的分區放到空閒分區列表中。在日誌中標記爲[GC pause (mixed)], 即年輕代和老年代會被同時收集。見下圖:

回收完以後就成了下面這個樣子:

​ CSet:收集集合(CSet) 是一組可被回收的分區的集合。 在CSet中存活的數據會在GC過程當中被移動到另外一個可用分區,CSet中的分區能夠來自eden空間、survivor空間、或者老年代。

分代G1模式下選擇CSet有兩種子模式, 分別對應Young GC和Mixed GC:

  • Young GC: CSet就是全部年輕代裏面的Region;
  • Mixed GC: CSet是全部年輕代裏的Region加上在全局併發標記階段標記出來的收益高的Region;

1.4. 擴展

1.4.1. 記憶集和卡表

​ 說到G1收集器不得不說記憶集(Remembered Set, 簡稱RSet)和卡表(Card Table),不少文章都會穿插在GC過程當中來講,若是不瞭解可能會看不下去,其實不介紹這兩個數據結構更容易瞭解GC流程。

​ 考慮這樣一個問題:YGC的時候針對的是所有新生代,那麼選GC Roots的時候,由於存在跨代引用的問題而很差肯定哪些是roots,其實不少root是在老年代。可是老年代那麼大,總不能每次都花那麼多時間掃描整個老年代,這就至關於掃描了整個堆了,由於新生代確定是要所有掃描的。所以在新生代引入了記憶集RSet這樣一個數據結構,用來記錄哪些Region中的對象指向了當前分區中的對象(記憶集是一種用於記錄從非收集區域指向收集區域的指針集合的抽象數據結構,用以免把整個老年代加進GC Roots掃描範圍),下面經過簡單的畫圖來理解。

​ 容易知道的是若是忽略老年代做爲根而不去掃描的話,顯然對象E就是垃圾對象,這就會致使誤判;可是也不能去掃描所有的老年代,因此在該Eden Region中使用的RSet,記錄了Old Region中有對象指向了本區域中的對象。說到這可能猜到了RSet是怎樣的數據結構了。沒錯,就是哈希表,key爲別的Region的起始地址,value則是一個集合,集合中的元素地址(實際上是卡表中元素的下標index)。

​ 一個卡表會將一個分區Region劃分爲固定大小的連續區域,每一個區域又稱爲卡。卡表一般爲字節數組,由卡的索引(數組下標)來標識每一個分區的地址。默認狀況下每一個卡都未被引用。當一個卡被引用的時候,則將該卡地址對應的數組下標的值標記爲0。同時根據須要在RSet中記錄下來,好比O指向了E,那麼在該Eden區的RSet中會記錄O在Old區域中卡表的索引,看下面的圖可能會明白我說的啥。

說明: 比較權威的書和部分文章寫的是記憶集是一種抽象,卡表則是其實現,二者的關係就像Map和HashMap,但因爲上面這種方式對於我來講更易於理解,因此就介紹了上面這種方式。

1.4.2. STAB和TAMS

​ 再說第二個問題:在併發標記階段如何保證收集線程與用戶線程互不干擾地運行?

首先要解決的是用戶線程改變對象引用關係時,必須保證其不能打破本來的對象圖結構,致使標記結果出現錯誤。

​ G1就是採用了SATB(Snapshot-At-The-Beginning,原始快照)算法來實現的:SATB能夠理解成在GC開始以前對堆內存裏的對象作一次快照,此時活的對象就認爲是活的,從而造成一個對象圖。在GC收集的時候,新生代的對象也認爲是活的對象,除此以外其餘不可達的對象都認爲是垃圾對象。SATB算法主要分紅如下三個步驟:

  1. 在開始標記的時候生成一個快照圖,標記存活對象;
  2. 在併發標記的時候全部被改變的對象入隊(在write barrier裏把全部舊的引用所指向的對象都變成非白的);
  3. 可能存在浮動垃圾,將在下次被收集;

​ 關於步驟2能夠這樣理解,假設存在這樣的關係A.b = B, C.b=null(此時B是白色的),併發標記中發生了這樣的變化A.b=null,C.b=B。 那麼這個時候因爲舊的引用A所指向的對象是B,因此將B標記爲非白。標記最後若是對象爲白色那就是垃圾對象,此外還有灰色和黑色(黑色是存活對象,灰色多是垃圾對象或者不是),這屬於三色標記算法內容,避免越說越糊塗就很少介紹(SATB其實依賴三色標記),下圖簡單展現了變化過程。

其次要解決的問題是,前面說了併發過程新分配的對象認爲是存活對象,那麼如何找到GC過程當中新分配的對象呢?

​ 每一個region記錄着兩個top-at-mark-start(TAMS指針, 分別爲prev TAMS和next TAMS。在TAMS以上的對象就是新分配的, 於是被視爲隱式marked。經過這種方式咱們就找到了在GC過程當中新分配的對象,並把這些對象認爲是活的對象。

最後,解決了對象在GC過程當中分配的問題,那麼在GC過程當中引用發生變化的問題怎麼解決呢?G1給出的解決辦法是經過Write Barrier。 Write Barrier就是對引用字段進行賦值作了額外處理,這個額外處理前面也說了,其實就是全部舊的引用所指向的對象都變成非白的,這樣就能夠了解到哪些引用對象發生了什麼樣的變化

1.5. G1相比CMS的優點

  • G1在壓縮空間方面有優點;
  • G1經過將內存空間分紅區域 (Region) 的方式避免內存碎片問題;
  • Eden、Survivor、Old區再也不固定, 在內存使用效率上來講更靈活;
  • G1能夠經過設置預期停頓時間(Pause Time) 來控制垃圾收集時間,避免應用雪崩現象G1在回收內存後會立刻同時作合併空閒內存的工做,而CMS默認是在STW(stop the world) 的時候作;
  • G1會在Young GC中使用, 而CMS只能在O區使用

2. G1日誌解讀與經驗分享

2.1. 日誌解讀重點信息解讀

​ 先看YGC日誌的樣子 GC pause (G1 Evacuation Pause) (young)

class space    used 5944K, capacity 6094K, committed 6144K, reserved 1048576K
2020-03-23T17:32:14.984+0800: 9.925: [GC pause (G1 Evacuation Pause) (young)2020-03-23T17:32:15.005+0800: 9.947: [SoftReference, 0 refs, 0.0000722 secs]2020-03-23T17:32:15.005+0800: 9.947: [WeakReference, 71 refs, 0.0000329 secs]2020-03-23T17:32:15.005+0800: 9.947: [FinalReference, 1660 refs, 0.0018747 secs]2020-03-23T17:32:15.007+0800: 9.949: [PhantomReference, 0 refs, 3 refs, 0.0000193 secs]2020-03-23T17:32:15.007+0800: 9.949: [JNI Weak Reference, 0.0000216 secs], 0.0252556 secs]
   [Parallel Time: 20.4 ms, GC Workers: 23]
      [GC Worker Start (ms): Min: 9925.6, Avg: 9925.7, Max: 9925.8, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 0.9, Avg: 2.3, Max: 10.1, Diff: 9.2, Sum: 53.9]
      [Update RS (ms): Min: 0.0, Avg: 1.2, Max: 2.8, Diff: 2.8, Sum: 27.9]
         [Processed Buffers: Min: 0, Avg: 3.3, Max: 13, Diff: 13, Sum: 75]
      [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.5, Diff: 0.5, Sum: 2.9]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.7, Max: 4.3, Diff: 4.3, Sum: 15.0]
      [Object Copy (ms): Min: 8.7, Avg: 14.6, Max: 18.6, Diff: 10.0, Sum: 336.1]
      [Termination (ms): Min: 0.0, Avg: 1.1, Max: 1.4, Diff: 1.4, Sum: 25.4]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 23]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.5]
      [GC Worker Total (ms): Min: 19.9, Avg: 20.1, Max: 20.2, Diff: 0.3, Sum: 461.7]
      [GC Worker End (ms): Min: 9945.8, Avg: 9945.8, Max: 9945.8, Diff: 0.1]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.4 ms]
   [Other: 4.4 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 2.6 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.3 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.7 ms]
   [Eden: 284.0M(284.0M)->0.0B(310.0M) Survivors: 20.0M->38.0M Heap: 336.1M(4096.0M)->94.7M(4096.0M)]
Heap after GC invocations=8 (full 0):
 garbage-first heap   total 4194304K, used 96923K [0x00000006c0000000, 0x00000006c0204000, 0x00000007c0000000)
region size 2048K, 19 young (38912K), 19 survivors (38912K)
 Metaspace       used 49410K, capacity 49926K, committed 50428K, reserved 1093632K
  class space    used 5944K, capacity 6094K, committed 6144K, reserved 1048576K
}
 [Times: user=0.38 sys=0.06, real=0.02 secs]
複製代碼

YGC的詳細過程就不說了,只說我的認爲關鍵的日誌信息:

[Object Copy (ms): Min: 8.7, Avg: 14.6, Max: 18.6, Diff: 10.0, Sum: 336.1]
[Times: user=0.38 sys=0.06, real=0.02 secs]
複製代碼

​ 這兩行表示拷貝對象花費的時間和耗時,主要看平均時間Avg和耗時real。若是你發現YGC耗時比較長,特別要留意這兩行信息,好比real=0.56 secs;Object Copy (ms): Avg: 501那你就知道是對象複製花費了太多時間,可是新生代基本都是招生夕死,那麼爲何還有那麼多對象存活呢?接下來就須要根據本身的業務具體分析了,後面會結合咱們項目遇到過的問題來進行分析。

[Eden: 284.0M(284.0M)->0.0B(310.0M) Survivors: 20.0M->38.0M Heap: 336.1M(4096.0M)->94.7M(4096.0M)]
複製代碼

​ 這行表示了內存回收的狀況,能夠看到的是Eden區域是所有會被回收的,倖存區漲了18M,最後堆內存狀況是從回收前的336.1M到回收後的94.7M。若是你發現間歇性的倖存區域從較小的內存佔用,回收後佔用飆升,那極可能就有問題了,也便是存在大量對象存活了。固然若是回收後堆內存仍是一直上漲,好比咱們這裏最大堆內存是4G,若是回收後一直飆到了2000M(通常是佔用45%左右)那你可能就要分析下是否正常了,好比是否常常有大對象申請內存,大對象進入了老年代而沒有獲得回收致使一直上升。不過通常這時會進行mixed gc了,具體的後面會分析遇到的一些問題。

​ 再來看mixed gc ,實際上是和YGC分析是很相似的,因爲全局併發標記中的初始標記是伴隨着YGC,因此在這一階段看到的實際上是YGC內容

2020-03-24T19:34:12.416+0800: 7388.991: [GC pause (G1 Evacuation Pause) (young) (initial-mark)2020-03-24T19:34:12.447+0800: 7389.022: [SoftReference, 0 refs, 0.0000844 secs]2020-03-24T19:34:12.447+0800: 7389.022: [WeakReference, 24 refs, 0.0000157 secs]2020-03-24T19:34:12.447+0800: 7389.022: [FinalReference, 20 refs, 0.0000212 secs]2020-03-24T19:34:12.447+0800: 7389.022: [PhantomReference, 0 refs, 0 refs, 0.0000098 secs]2020-03-24T19:34:12.447+0800: 7389.022: [JNI Weak Reference, 0.0000400 secs], 0.0336560 secs]
   [Parallel Time: 28.3 ms, GC Workers: 18]
      [GC Worker Start (ms): Min: 7388993.6, Avg: 7388994.1, Max: 7388994.7, Diff: 1.2]
      ... 內容同YGC
2020-03-24T19:34:12.450+0800: 7389.025: [GC concurrent-root-region-scan-start]
2020-03-24T19:34:12.450+0800: 7389.025: Total time for which application threads were stopped: 0.0366523 seconds, Stopping threads took: 0.0006589 seconds
2020-03-24T19:34:12.461+0800: 7389.036: [GC concurrent-root-region-scan-end, 0.0111133 secs]
2020-03-24T19:34:12.461+0800: 7389.036: [GC concurrent-mark-start]
2020-03-24T19:34:12.917+0800: 7389.493: [GC concurrent-mark-end, 0.4565315 secs]
2020-03-24T19:34:12.919+0800: 7389.494: [GC remark 2020-03-24T19:34:12.919+0800: 7389.494: [Finalize Marking, 0.0016538 secs] 2020-03-24T19:34:12.920+0800: 7389.496: [GC ref-proc2020-03-24T19:34:12.920+0800: 7389.496: [SoftReference, 2129 refs, 0.0008546 secs]2020-03-24T19:34:12.921+0800: 7389.497: [WeakReference, 1891 refs, 0.0006409 secs]2020-03-24T19:34:12.922+0800: 7389.497: [FinalReference, 947 refs, 0.0009644 secs]2020-03-24T19:34:12.923+0800: 7389.498: [PhantomReference, 2 refs, 279 refs, 0.0002176 secs]2020-03-24T19:34:12.923+0800: 7389.498: [JNI Weak Reference, 0.0000991 secs], 0.0030448 secs] 2020-03-24T19:34:12.923+0800: 7389.499: [Unloading, 0.0516170 secs], 0.0587406 secs]
 [Times: user=0.20 sys=0.67, real=0.06 secs]
2020-03-24T19:34:12.978+0800: 7389.553: Total time for which application threads were stopped: 0.0604210 seconds, Stopping threads took: 0.0002143 seconds
2020-03-24T19:34:12.979+0800: 7389.554: [GC cleanup 1993M->1296M(4096M), 0.0411176 secs]
 [Times: user=0.04 sys=0.31, real=0.05 secs]
2020-03-24T19:34:13.020+0800: 7389.596: Total time for which application threads were stopped: 0.0426097 seconds, Stopping threads took: 0.0001096 seconds
2020-03-24T19:34:13.020+0800: 7389.596: [GC concurrent-cleanup-start]
2020-03-24T19:34:13.021+0800: 7389.597: [GC concurrent-cleanup-end, 0.0009866 secs]
2020-03-24T19:36:25.421+0800: 7521.996: [GC pause (G1 Evacuation Pause) (mixed)2020-03-24T19:36:25.438+0800: 7522.013: [SoftReference, 0 refs, 0.0000595 secs]2020-03-24T19:36:25.438+0800: 7522.013: [WeakReference, 45 refs, 0.0000194 secs]2020-03-24T19:36:25.438+0800: 7522.013: [FinalReference, 0 refs, 0.0000086 secs]2020-03-24T19:36:25.438+0800: 7522.013: [PhantomReference, 0 refs, 5 refs, 0.0000110 secs]2020-03-24T19:36:25.438+0800: 7522.013: [JNI Weak Reference, 0.0000482 secs], 0.0193059 secs]
   [Parallel Time: 13.0 ms, GC Workers: 18]
      [GC Worker Start (ms): Min: 7521999.3, Avg: 7521999.7, Max: 7522000.2, Diff: 0.9]
      ... 內容同YGC

複製代碼

​ 須要注意的是GC pause (G1 Evacuation Pause) (young) (initial-mark)和GC pause (G1 Evacuation Pause) (mixed)之間可能會穿插YGC,這兩段的內容和YGC是同樣的因此不須要解釋了。關於mixed gc的步驟從日誌信息裏面很容易讀出來,好比初始標記GC pause (G1 Evacuation Pause) (young) (initial-mark), 根Region掃描,併發標記等,對着 1.3的步驟介紹很容易看明白。

​ 一般來講當某次回收後(大部分是YGC)堆內存佔用達到45%左右,後面若是沒降下去那很快就會進行mixed gc來整理老年代了。若是程序正常的話mixed gc頻率應該是很低的,咱們的項目可能還有點問題,可是通常也就30分鐘一次。若是你的項目出現的頻率很高,好比1-5分鐘一次,那你就可能須要考慮是否出現了問題。頻繁的mixed gc極可能說明了老年代內存得不到釋放,同時可能還在增長,可能最後內存回收的速度跟不上分配的速度那就要full gc了,動不動就會十幾秒,並且會不斷進行下去,因爲full gc是單線程STW的,此時服務基本就不可用了。因此full gc是要極力去避免的。

2.2. 一些參數介紹與重要提示

-XX:+CrashOnOutOfMemoryError         若是啓用了此選項,當出現內存不足錯誤時,JVM將崩潰併產生文本和二進制崩潰文件(若是啓用了核心文件),jdk8加的。
-XX:GCLogFileSize=xxxxxxxxx          GC日誌文件大小上限
-XX:NumberOfGCLogFiles=100           GC日誌文件數量上限
-XX:+HeapDumpOnOutOfMemoryError      內存溢出的時候dump文件
-XX:InitialHeapSize=4294967296       初始堆內存大小
-XX:MaxDirectMemorySize=805306368    最大直接內存大小
-XX:MaxHeapSize=4294967296           最大堆內存大小
-XX:MaxGCPauseMillis=200             GC時候STW最大停頓時間
-XX:+PrintGCDateStamps               打印GC時間(當前時間,好比2020-03-23T17:33:07.939+0800)
-XX:+PrintGCDetails                  輸出GC詳細日誌
-XX:+PrintGCTimeStamps               打印GC時間(jvm啓動直到垃圾收集發生所經歷的時間,好比63表示經歷了64秒)
-XX:+UseG1GC                         使用G1收集器
-XX:G1HeapRegionSize=n               每一個Region的大小
-G1HeapWastePercent                  在global concurrent marking結束以後,咱們能夠知道old generegions中有多少空間要被回收,在每次YGC以後和再次發生Mixed GC以前,會檢查垃圾佔比是否達到此參數,只有達到了,下次纔會發生Mixed GC
-G1MixedGCLiveThresholdPercent       old generation region中的存活對象的佔比,只有在此參數之下,纔會被選入CSet
-G1MixedGCCountTarget 				一次global concurrent marking以後,最多執行MixedGC的次數
複製代碼

​ 寫參數的時候記得初始堆內存和最大堆內存要同樣,以免每次垃圾回收完成後JVM從新分配內存,若是不一致極可能會致使full gc問題。

​ 使用的G1收集器,不要去指定新生代(-Xmn)和老年代的大小,讓收集器動態調整便可。不然可能帶來下面兩個問題

  • G1將再也不考慮暫停時間目標。所以,從本質上講,設置年輕一代的大小會阻礙暫停時間目標的實現;
  • G1再也不可以根據須要擴展和收縮年輕一代的空間。由於大小是固定的,因此不能改變大小;

​ 經過-XX:MaxGCPauseMillis=x能夠設置啓動應用程序暫停的時間,G1在運行的時候會根據這個參數選擇CSet來知足響應時間的設置。通常狀況下這個值設置到100ms或者200ms都是能夠的(不一樣狀況下會不同),但若是設置成50ms就不太合理。暫停時間設置的過短,就會致使出現G1跟不上垃圾產生的速度。最終退化成Full GC。因此對這個參數的調優是一個持續的過程,逐步調整到最佳狀態。對於如何設置官方有個tips:時間小於等於程序響應客戶端的時間的10%,好比http服務響應瀏覽器的時間通常就1000ms,那麼能夠設置停頓時間是100ms,固然具體的還得去調試看看。記住:暫停時間是一個目標,並不能保證老是能達到。

​ 最後須要注意是否有Evacuation Failurefull x,這都是不該該出現的,出現了要當即解決掉。

2.3. 經驗分享

2.3.1. Full GC經驗分享

​ 當時運維人員通知了項目不停full gc後,首先登機器dump文件,而後重啓機器暫時保住線上恢復,而後將 dump文件發送到本機使用Mat進行分析。

1.dump文件:

- 命令: jmap -dump:format=b,file=./myfile 324576
- myfile爲dump的文件名,324576爲pid
    關於jmap還有兩個實用的命令:
  1. jmap -histo:live pid   查看存活對象分佈狀況(可能會觸發full gc不要在線上使用,不加live能夠,加了live會強制執行一次full gc);
  2. jmap -heap pid    查看heap參數,使用狀況等;
複製代碼

2.dump文件下載到本地:

- 命令:scp -r ./myfile  tom@192.168.25.128: /home/tom/mydir
- 下載完以後給文件名加個.hprof後綴(或者dump的時候就加上),由於mat打開dump文件的時候只選擇hprof文件。
複製代碼

3.下載MAT並進行分析:

- 下載地址: https://www.eclipse.org/mat/downloads.php, 注意選國內鏡像;
- 下載完以後基本上是打不開大點的dump文件(該文件有6個G),因此須要使用命令行啓動MAT同時指定最大堆內存參數;
- 進入到安裝目錄,啓動命令:MemoryAnalyzer.exe -vmargs -Xmx8g    
複製代碼

打開後看到

​ 顯然佔用3G的對象是有問題的,點擊左上角的圖標查看對象分佈,找到佔用內存過大的對象

​ 基本一個對象佔了5M左右,到這裏基本就知道問題在哪了,找到代碼去解決BUG吧,其實不少時候full gc仍是代碼寫的有問題,這樣的代碼是真的存在的,遇到幾回了。 有時候須要注意使用System.gc()致使的full gc,這個會有相似提示Full GC(System.gc()),須要根據業務考慮是否要避免,由於大部分是引用的包,本身確定是不會寫這麼個東西的,好比項目剛啓動的時候可能作一些加載啥的會用一次,後續不會再出現那就沒問題。

2.3.2. 日誌分析

​ 對於上面的full gc問題其實從GC日誌一開始也分析出來了,只是當時還沒出現full gc就打算次日去解決,可是晚了。根據公司老司機分享的經驗,首先看下eden區域狀況

cat gc.log | grep 'Eden'|less
複製代碼

img

​ 回收後基本都在3000M左右,老年代佔用了那麼多基本上會大批量的進行mixed gc,查看下頻率

$ cat gc.log | grep 'mixed'|awk '{print $1}'
$ cat gc.log | grep 'real'|awk '{print $4}'|sort -n -k 2 -t = # 這條能夠不看
複製代碼

img

​ 其實這個頻率是不合理的,幾乎是一分鐘一次 mixed gc,再來看gc 內容:從mixed gc往下找,發現了mixed gc前的yong gc總會有大對象內存分配申請。

img

​ 說明不停的有大對象內存申請,而後大對象會直接進入老年代Humongous 區(若是是5M那麼得三個連續的Humongous 區),致使內存不斷上漲,可是得不到釋放後又不斷申請。若是使用jmap -histo pid基本就能查到是啥子對象了。

2.4.2. YGC時間過長問題分析

​ 一般狀況下因爲新生對象存活率低,YGC的時間是很短的,由於不須要拷貝大量存活對象。可是還真見過兩次YGC時間過長的問題(1-3秒),因爲會STW因此這個問題是須要解決的。以前有人反饋服務存在間歇性抖動問題(內存也存在抖動,當時只分配了2G內存,因此問題很是明顯,後續是改了4G才改善了不少,下面的分析則是基於改善後的分析)。

一樣仍是先grep 'Eden'區域:

前面還好都是上圖那樣,屬於正常。可是繼續翻發現了問題:

​ 內存忽然陡增,固然幾回gc後又下來了,顯然這裏一定作了mixed gc(多是幾回),查一些mixed gc頻率發現是每30分鐘一次(若是對堆內存有監控那更是一目瞭然),到這裏能夠猜想是否是什麼定時任務致使的(好比lettuce的拓撲結構每隔n分鐘刷新一次也須要考慮),可使用jmap查看對象佔用內存狀況,而後肯定該對象屬於哪一塊功能從而肯定問題點,其實最後發現就是定時任務致使的。

​ 進入到日誌內容的時候能夠發現,大量的內存拷貝佔了過多的時間

​ 由於定時任務致使了建立不少對象分配在Eden區域,gc後對這些對象進行拷貝,致使survivor區域暴漲,同時可能由於survivor不能徹底容納這些對象,致使直接進入到了老年代,從而佔用內存一下漲了1G,此時2G內存已經佔了最大堆內存的一半了,接下來基本是會進行mixed gc來回收所有新生代的和部分有價值的老年代了。

相關文章
相關標籤/搜索