JDK8 的FullGC 之 metaspace


前言:

由於最近寫的程序在運行一段時間后出現高cpu,然后不可用故進而進行排查,最終定位到由於metaspace引起fullgc,不斷的fullgc又占用大量cpu導致程序最終不可用。下面就是這次過程的分析排查和總結,便於以后溫故,同時也希望能給遇到同樣問題的同學一些參考。

一 jvm的內存分配情況:

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


jvm內存young區圖.png

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

發生在 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進制為68c5

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

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

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

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

"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)來避免。
5、統計得到的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。
6、堆中分配很大的對象
所謂大對象,是指需要大量連續內存空間的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM