JDK8 的FullGC 之 metaspace

前言:

因爲最近寫的程序在運行一段時間後出現高cpu,而後不可用故進而進行排查,最終定位到因爲metaspace引發fullgc,不斷的fullgc又佔用大量cpu致使程序最終不可用。下面就是此次過程的分析排查和總結,便於之後溫故,同時也但願能給遇到一樣問題的同窗一些參考。html

一 jvm的內存分配狀況:

Eden Survivor1 Survivor2 Tenured
Tenured 包含perm jdk<=7java

 

jvm內存young區圖.png

gc類型分爲:minor gc 和 major gc ,major的速度比minor慢10倍至少vim

發生在 young(主要是Survivor)區的gc稱爲 minor gc
發生在 old(Tenured)區的gc稱爲 major gc數組

1.問題描述

jstat -gcutil 26819 
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 43.75 0.00 42.22 67.19 50.93 4955 30.970 4890 3505.049 3536.020
能夠看到M(metaSpace使用率)的值是67.19,metaSpace使用率爲67.19;O爲42.22,old區使用率爲42.22併發

top -H -p 26819 
26821 appdev 20 0 6864m 1.2g 13m R 87.6 7.5 53:40.18 java
26822 appdev 20 0 6864m 1.2g 13m R 87.6 7.5 53:41.40 java
26823 appdev 20 0 6864m 1.2g 13m R 87.6 7.5 53:43.64 java
26824 appdev 20 0 6864m 1.2g 13m R 85.6 7.5 53:41.59 java
26825 appdev 20 0 6864m 1.2g 13m R 85.6 7.5 53:43.82 java
26826 appdev 20 0 6864m 1.2g 13m R 85.6 7.5 53:40.47 java
26827 appdev 20 0 6864m 1.2g 13m R 85.6 7.5 53:45.05 java
26828 appdev 20 0 6864m 1.2g 13m R 83.6 7.5 53:39.08 java
能夠發現26821到26828的cpu使用率很高,26821轉爲16進製爲68c5oracle

jstack 26819 > 26819.text 
vim 26819.text 而後搜索68c5-68cc
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f0aa401e000 nid=0x68c5 runnableapp

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f0aa4020000 nid=0x68c6 runnablejvm

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f0aa4021800 nid=0x68c7 runnableide

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f0aa4023800 nid=0x68c8 runnablepost

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00007f0aa4025800 nid=0x68c9 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00007f0aa4027000 nid=0x68ca runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00007f0aa4029000 nid=0x68cb runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00007f0aa402a800 nid=0x68cc runnable

能夠發現一致是full gc的線程在執行,佔用cpu較高的資源,而且一致持續,代表一直達到了full gc的條件可是又不能回收掉內存從而佔用大量cpu,致使程序不可用。

查看啓動配置參數以下:
-Xms1000m -Xmx1000m -XX:MaxNewSize=256m -XX:ThreadStackSize=256 -XX:MetaspaceSize=38m -XX:MaxMetaspaceSize=380m
分析程序的邏輯,程序會加載不少jar到內存,程序是一個公共服務,不少同事會上傳jar,而後程序把jar加載到classloader進行分析並保存。

2.問題分析:

根據jdk8的metaspace的fullgc的觸發條件,初始metaspacesize是38m意味着當第一次加載的class達到38m的時候進行第一次gc(根據JDK 8的特性,G1和CMS都會很好地收集Metaspace區(通常都伴隨着Full GC)。),而後jvm會動態調整 (gc後會進行調整)metaspacesize的大小。

JDK8: Metaspace 
In JDK 8, classes metadata is now stored in the native heap
and this space is called Metaspace. There are some new flags added for
Metaspace in JDK 8:
-XX:MetaspaceSize=<NNN>
where <NNN> is the initial amount of space(the initial
high-water-mark) allocated for class metadata (in bytes) that may induce a
garbage collection to unload classes. The amount is approximate. After the
high-water-mark is first reached, the next high-water-mark is managed by
the garbage collector
-XX:MaxMetaspaceSize=<NNN>
where <NNN> is the maximum amount of space to be allocated for class
metadata (in bytes). This flag can be used to limit the amount of space
allocated for class metadata. This value is approximate. By default there
is no limit set.
-XX:MinMetaspaceFreeRatio=<NNN>
where <NNN> is the minimum percentage of class metadata capacity
free after a GC to avoid an increase in the amount of space
(high-water-mark) allocated for class metadata that will induce a garbage
collection.
-XX:MaxMetaspaceFreeRatio=<NNN>
where <NNN> is the maximum percentage of class metadata capacity
free after a GC to avoid a reduction in the amount of space
(high-water-mark) allocated for class metadata that will induce a garbage
collection.
By default class
metadata allocation is only limited by the amount of available native memory. We
can use the new option MaxMetaspaceSize to limit the amount of native memory
used for the class metadata. It is analogous(相似) to MaxPermSize. A garbage collection is induced to collect the dead classloaders
and classes when the class metadata usage reaches MetaspaceSize (12Mbytes on
the 32bit client VM and 16Mbytes on the 32bit server VM with larger sizes on
the 64bit VMs). Set MetaspaceSize to a higher value to delay the induced
garbage collections. After an induced garbage collection, the class metadata usage
needed to induce the next garbage collection may be increased.

根據這段描述能夠知道:
1.當metadata usage reaches MetaspaceSize(默認MetaspaceSize在64爲server上是20.8m)就會觸發gc;
2.XX:MinMetaspaceFreeRatio是用來避免下次申請的空閒metadata大於暫時擁有的空閒metadata而觸發gc,舉個例子就是,當metaspacesize的使用大小達到了第一次設置的初始值6m,這時進行進行擴容(以前已經作過MinMetaspaceExpansion和MaxMetaspaceExpansion擴展,但仍是失敗),而後gc後,因爲回收調的內存很小,而後計算((待commit內存)/(待commit內存+已經commmited內存) ==40%,(待commit內存+已經commmited內存)大於了metaspaceSize那麼將嘗試作擴容,也就是增大觸發metaspaceGC的閾值,不過這個增量至少是MinMetaspaceExpansion纔會作,否則不會增長這個閾值) ,這個參數主要是爲了不觸發metaspaceGC的閾值和gc以後committed的內存的量比較接近,因而將這個閾值(metaspaceSize)進行擴大,儘可能減少下次gc的概率。
3.同理-XX:MaxMetaspaceFreeRatio(默認70)是用來避免下次申請的空閒metadata很小,遠遠小於如今的空閒內存從而致使gc。主要做用是減少沒必要要的內存佔用空間。

jdk8的metaspace引起的fullgc: 
jdk8使用metaspace代替以前的perm,metaspace使用native memory,默認狀況下使用的最大大小是系統內存大小,固然也可使用-XX:MaxMetaspaceSize設置最大大小,這個設置和以前的max perm size是同樣的。同時當設置-XX:MaxMetaspaceSize這個參數後,咱們也能夠實現和max perm引發oom的問題。
We can achieve the famed OOM error by setting the MaxMetaspaceSize argument to JVM and running the sample program provided.
metaspaceSize默認初始大小:
MetaspaceSize (12Mbytes on the 32bit client VM and 16Mbytes on the 32bit server VM with larger sizes on the 64bit VMs).
能夠經過-XX:MetaspaceSize 設置咱們須要的初始大小,設置大點能夠增長第一次達到full gc的時間。

ps:下面是調整了下參數重啓的進程,和上面的進程Id有出入。
jstat -gc 1706 
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
31744.0 32768.0 0.0 21603.6 195584.0 192805.8 761856.0 384823.3 467712.0 309814.3 65536.0 36929.1 101 2.887 3 1.224 4.112
分析:MC是已經commited的內存,MU是當前使用的內存。這裏有個疑惑就是MC是否是就是metaspace已經總共使用的內存,由於這個值已經達到了maxmetaspacesize,同時爲何mu不是和mc同樣我猜想是因爲碎片內存致使,這裏有知道的同窗能夠告訴我下。在達到maxmetaspacesize的時候執行了3次fullgc。可是接下來因爲不斷申請內存,不斷fullgc,fullgc不能回收內存,這時候fullgc的頻率增大不少。在接下來 top -H -p 1706查看cpu能夠看到大量高cpu進程,經過jstack查看都是在進行fullgc。

jmap -clstats 1706 
第一次:total = 131 8016 13892091 N/A alive=45, dead=86 N/A
第二次:total = 1345 37619 77242171 N/A alive=1170, dead=175 N/A
alive的classloader基本都是本身建立的
classLoader不斷增長,每次gc並無回收掉classloader
VM中的Class只有知足如下三個條件,才能被GC回收,也就是該Class被卸載(unload):

  • 該類全部的實例都已經被GC,也就是JVM中不存在該Class的任何實例。
  • 加載該類的ClassLoader已經被GC。ClassLoader被回收須要全部ClassLoader的全部類的實例都被回收。
  • 該類的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方經過反射訪問該類的方法

jcmd 1706 GC.class_stats | awk '{print $13}' | sort | uniq -c | sort -nrk1 > topclass.txt 

class.png

經過自定義的classloader加載的類重複屢次,而且數量一直增長。
看到大量的類重複數量

gc日誌分析:

第一次fullgc: 
[Heap Dump (before full gc): , 0.4032181 secs]2018-01-10T16:37:44.658+0800: 21.673: [Full GC (Metadata GC Threshold) [PSYoungGen: 14337K->0K(235520K)] [ParOldGen: 18787K->30930K(761856K)] 33125K->30930K(997376K), [Metaspace: 37827K->37827K(1083392K)], 0.1360661 secs] [Times: user=0.65 sys=0.04, real=0.14 secs]
主要是Metaspace這裏:[Metaspace: 37827K->37827K(1083392K)] 達到了咱們設定的初始值38m,而且gc並無回收掉內存。1083392K這個值懷疑是使用了CompressedClassSpaceSize = 1073741824 (1024.0MB)這個致使的。
第四次fullgc: 
[Heap Dump (before full gc): , 5.3642805 secs]2018-01-10T16:53:43.811+0800: 980.825: [Full GC (Metadata GC Threshold) [PSYoungGen: 21613K->0K(231424K)] [ParOldGen: 390439K->400478K(761856K)] 412053K->400478K(993280K), [Metaspace: 314108K->313262K(1458176K)], 1.2320834 secs] [Times: user=7.86 sys=0.06, real=1.23 secs]
主要是Metaspace這裏:[Metaspace: 314108K->313262K(1458176K)]達到了咱們設定的MinMetaspaceFreeRatio,而且gc幾乎沒有回收掉內存。1458176K這個值是CompressedClassSpaceSize = 1073741824 (1024.0MB)和 MaxMetaspaceSize = 503316480 (480.0MB)的和。

後面就是頻率很快的重複fullgc。

3.問題解決:

有了以上基礎,就知道怎麼解決此次遇到的問題了。
總結下緣由:classloader不斷建立,classloader不斷加載class,以前的classloader和class在fullgc的時候沒有回收掉。

  1. 程序避免建立重複classloader,減小建立classLoader的數量。
  2. 增大XX:MinMetaspaceFreeRatio(默認40)的大小,能夠看到如今是(100-67.19)。
  3. 設置更大的maxmetaspaceSize。

jdk8metadataspace參考: 
http://www.sczyh30.com/posts/Java/jvm-metaspace/
http://blog.csdn.net/ouyang111222/article/details/53688986
http://lovestblog.cn/blog/2016/10/29/metaspace/
https://bugs.openjdk.java.net/browse/JDK-8151845
http://blog.csdn.net/ouyang111222/article/details/53688986
https://blogs.oracle.com/poonam/about-g1-garbage-collector%2c-permanent-generation-and-metaspace
http://zhuanlan.51cto.com/art/201706/541920.htm
http://blog.yongbin.me/2017/03/20/jaxb_metaspace_oom/

延伸閱讀

jdk中觸發gc的條件:

1,System.gc()方法的調用 
system.gc(), 此方法的調用是建議JVM進行Full GC,雖然只是建議而非必定,但不少狀況下它會觸發 Full GC,從而增長Full GC的頻率,也即增長了間歇性停頓的次數。強烈建議能不使用此方法就別使用,讓虛擬機本身去管理它的內存,可經過經過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。
2,老年代代空間(old/Tenured)不足 
老年代空間只有在新生代對象轉入及建立爲大對象、大數組時纔會出現不足的現象,當執行Full GC後空間仍然不足,則拋出以下錯誤:java.lang.OutOfMemoryError: Java heap space 爲避免以上兩種情況引發的Full GC,調優時應儘可能作到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要建立過大的對象及數組。
3,永生區(perm)空間不足(jdk<=7 ,在jdk8裏面是metaspace ,後面會重點描述) 
JVM規範中運行時數據區域中的方法區,在HotSpot虛擬機中又被習慣稱爲永生代或者永生區,Permanet Generation中存放的爲一些class的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用CMS GC的狀況下也會執行Full GC。若是通過Full GC仍然回收不了,那麼JVM會拋出以下錯誤信息:java.lang.OutOfMemoryError: PermGen space 爲避免Perm Gen佔滿形成Full GC現象,可採用的方法爲增大Perm Gen空間或轉爲使用CMS GC。
4,CMS GC時出現promotion failed和concurrent mode failure 
對於採用CMS進行老年代GC的程序而言,尤爲要注意GC日誌中是否有promotion failed和concurrent mode failure兩種情況,當這兩種情況出現時可能會觸發Full GC。promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入老年代,而此時老年代也放不下形成的;concurrent mode failure是在執行CMS GC的過程當中同時有對象要放入老年代,而此時老年代空間不足形成的(有時候「空間不足」是CMS GC時當前的浮動垃圾過多致使暫時性的空間不足觸發Full GC)。對措施爲:增大survivor space、老年代空間或調低觸發併發GC的比率(-XX:CMSInitiatingOccupancyFraction=70,預留空間爲70%),但在JDK 5.0+、6.0+的版本中有可能會因爲JDK的bug29致使CMS在remark完畢後好久才觸發sweeping動做。對於這種情況,可經過設置-XX: CMSMaxAbortablePrecleanTime=5(單位爲ms)來避免。
五、統計獲得的Minor GC晉升到舊生代(Eden到S2和S1到S2的和)的平均大小大於老年代的剩餘空間 
這是一個較爲複雜的觸發狀況,Hotspot爲了不因爲新生代對象晉升到舊生代致使舊生代空間不足的現象,在進行Minor GC時,作了一個判斷,若是以前統計所獲得的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。例如程序第一次觸發Minor GC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,若是小於6MB,則執行Full GC。當新生代採用PS GC時,方式稍有不一樣,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。
除了以上4種情況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,默認狀況下會一小時執行一次Full GC。可經過在啓動時經過- java -Dsun.rmi.dgc.client.gcInterval=3600000來設置Full GC執行的間隔時間或經過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。
六、堆中分配很大的對象 
所謂大對象,是指須要大量連續內存空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩餘空間,可是沒法找到足夠大的連續空間來分配給當前對象,此種狀況就會觸發JVM進行Full GC。
爲了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用於在「享受」完Full GC服務以後額外免費贈送一個碎片整理的過程,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用於設置在執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的。

延伸閱讀參考: 
http://engineering.xueqiu.com/blog/2015/06/25/jvm-gc-tuning/
http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
http://blog.csdn.net/chenleixing/article/details/46706039

相關文章
相關標籤/搜索