由「Metaspace容量不足触发CMS GC」从而引发的思考
轉(zhuǎn)載自??由「Metaspace容量不足觸發(fā)CMS GC」從而引發(fā)的思考
某天早上,毛老師在群里問「cat 上怎么看 gc」。
?
好好的一個(gè)群
看到有 GC 的問題,立馬做出小雞搓手狀。
之后毛老師發(fā)來一張圖。
?
老年代內(nèi)存占用情況
圖片展示了老年代內(nèi)存占用情況。
第一個(gè)大陡坡是應(yīng)用發(fā)布,老年代內(nèi)存占比下降,很正常。
第二個(gè)小陡坡,老年代內(nèi)存占用突然下降,應(yīng)該是發(fā)生了老年代 GC。
但奇怪的是,此時(shí)老年代內(nèi)存占用并不高,發(fā)生 GC 并不是正常現(xiàn)象。
于是,毛老師查看了 GC log。
?
GC log
從 GC log 中可以看出,老年代發(fā)生了一次 CMS GC。
但此時(shí)老年代內(nèi)存使用占比 = 234011K / 2621440k ≈ 9%。
而 CMS 觸發(fā)的條件是:
老年代內(nèi)存使用占比達(dá)到?CMSInitiatingOccupancyFraction,默認(rèn)為 92%,
毛老師設(shè)置的是 75%。
1-XX:CMSInitiatingOccupancyFraction?=?75
于是排除老年代占用過高的可能。
接著分析內(nèi)存狀況。
?
Metaspace 內(nèi)存占用情況
毛老師發(fā)現(xiàn)在老年代發(fā)生 GC 時(shí),Metaspace 的內(nèi)存占用也一起下降。
于是懷疑是 Metaspace 占用達(dá)到了設(shè)置的參數(shù) MetaspaceSize,發(fā)生了 GC。
查看 JVM 參數(shù)設(shè)置,MetaspaceSize 參數(shù)被設(shè)置為128m。
1-XX:MetaspaceSize?=?128m?-XX:MaxMetaspaceSize?=?256m
問題的原因被集中在 Metaspace 上。
毛老師查看另外一個(gè)監(jiān)控工具,發(fā)生小陡坡的縱坐標(biāo)的確接近 128m。
此時(shí),引發(fā)出另一個(gè)問題:
Metaspace 發(fā)生 GC,為何會(huì)引起老年代 GC。
于是,想到之前看過?阿飛Javaer?的文章 《JVM參數(shù)MetaspaceSize的誤解》。
其中有幾個(gè)關(guān)鍵點(diǎn):
Metaspace 在空間不足時(shí),會(huì)進(jìn)行擴(kuò)容,并逐漸達(dá)到設(shè)置的 MetaspaceSize。
Metaspace 擴(kuò)容到 -XX:MetaspaceSize 參數(shù)指定的量,就會(huì)發(fā)生 FGC。
如果配置了 -XX:MetaspaceSize,那么觸發(fā) FGC 的閾值就是配置的值。
如果 Old 區(qū)配置 CMS 垃圾回收,那么擴(kuò)容引起的 FGC 也會(huì)使用 CMS 算法進(jìn)行回收。
其中的關(guān)鍵點(diǎn)是:
如果老年代設(shè)置了 CMS,則 Metasapce 擴(kuò)容引起的 FGC 會(huì)轉(zhuǎn)變成一次 CMS。
查看毛老師配置的 JVM 參數(shù),果然設(shè)置了 CMS GC。
1-XX:+UseConcMarkSweepGC
于是,解決問題的方法是調(diào)整 -XX:MetaspaceSize = 256m。
從監(jiān)控來看,設(shè)置 -XX:MaxMetaspaceSize = 256m 已經(jīng)足夠。
因?yàn)楹笃诓⒉粫?huì)引發(fā) CMS GC。
GC 的問題算是解決了,但同時(shí)引發(fā)了以下幾點(diǎn)思考:
Metaspace 分配和擴(kuò)容有什么規(guī)律?
JDK 1.8 中的 Metaspace 和 JDK 1.7 中的 Perm 區(qū)有什么區(qū)別?
老年代回收設(shè)置成非 CMS 時(shí),Metaspace 占用到達(dá) -XX:MetaspaceSize 會(huì)引發(fā)什么 GC?
如何制造 Metasapce 內(nèi)存占用上升?
關(guān)于這個(gè)問題一和問題二,阿飛Javaer?已經(jīng)解釋的比較清楚。
對(duì)于 Metaspce,其初始大小并不等于設(shè)置的 -XX:MetaspaceSize 參數(shù)。
隨著類的加載,Metaspce 會(huì)不斷進(jìn)行擴(kuò)容,直到達(dá)到 -XX:MetaspaceSize 觸發(fā) GC。
而至于如何設(shè)置 Metaspace 的初始大小,目前的確沒有辦法。
在 openjdk 的 bug 列表中,找到一個(gè)?關(guān)于 Metaspace 初始大小的 bug,并且尚未解決。
?
Add JVM option to set initial Metaspace size
對(duì)于問題二,?阿飛Javaer?在文章中也進(jìn)行了說明。
Perm 的話,我們通過配置 -XX:PermSize 以及 -XX:MaxPermSize 來控制這塊內(nèi)存的大小。
JVM 在啟動(dòng)的時(shí)候會(huì)根據(jù) -XX:PermSize 初始化分配一塊連續(xù)的內(nèi)存塊。
這樣的話,如果 -XX:PermSize 設(shè)置過大,就是一種赤果果的浪費(fèi)。
關(guān)于 Metaspace,JVM 還提供了其余一些設(shè)置參數(shù)。
可以通過以下命令查看。
1java?-XX:+PrintFlagsFinal?-version?|?grep?Metaspace
關(guān)于 Metaspace 更多的內(nèi)容,可以參考笨神的文章:《JVM源碼分析之Metaspace解密》。
問題三
Metaspace 占用到達(dá) -XX:MetaspaceSize 會(huì)引發(fā)什么?
已經(jīng)知道,當(dāng)老年代回收設(shè)置成 CMS GC 時(shí),會(huì)觸發(fā)一次 CMS GC。
那么如果不設(shè)置為 CMS GC,又會(huì)發(fā)生什么呢?
使用以下配置進(jìn)行一個(gè)小嘗試,然后查看 GC log。
1-Xmx2048m?-Xms2048m?-Xmn1024m?
2-XX:MetaspaceSize=40m?-XX:MaxMetaspaceSize=128m
3-XX:+PrintGCDetails?-XX:+PrintGCDateStamps?
4-XX:+PrintHeapAtGC?-Xloggc:d:/heap_trace.txt
該配置并未設(shè)置 CMS GC,JDK 1.8 默認(rèn)的老年代回收算法為 ParOldGen。
本文測(cè)試的應(yīng)用在啟動(dòng)完成后,占用 Metaspace 空間約為 63m,可通過 jstat 命令查看。
于是,設(shè)置 -XX:MetaspaceSize = 40m,期望發(fā)生一次 GC。
從 GC log 中,可以找到以下關(guān)鍵日志。
1[GC?(Metadata?GC?Threshold)?
2[PSYoungGen:?360403K->47455K(917504K)]?360531K->47591K(1966080K),?0.0343563?secs]?
3[Times:?user=0.08?sys=0.00,?real=0.04?secs]?
4
5[Full?GC?(Metadata?GC?Threshold)?
6[PSYoungGen:?47455K->0K(917504K)]?
7[ParOldGen:?136K->46676K(1048576K)]?47591K->46676K(1966080K),?
8[Metaspace:?40381K->40381K(1085440K)],?0.1712704?secs]?
9[Times:?user=0.42?sys=0.02,?real=0.17?secs]?
可以看出,由于 Metasapce 到達(dá) -XX:MetaspaceSize = 40m 時(shí)候,觸發(fā)了一次 YGC 和一次 Full GC。
一般而言,我們對(duì) Full GC 的重視度比對(duì) YGC 高很多。
所以一般都會(huì)直描述,當(dāng) Metasapce 到達(dá) -XX:MetaspaceSize 時(shí)會(huì)觸發(fā)一次 Full GC。
問題四
如何人工模擬 Metaspace 內(nèi)存占用上升?
Metaspace 是 JDK 1.8 之后引入的一個(gè)區(qū)域。
有一點(diǎn)可以肯定的,Metaspace 會(huì)保存類的描述信息。
JVM 需要根據(jù) Metaspace 中的信息,才能找到堆中類 java.lang.Class 所對(duì)應(yīng)的對(duì)象。(有點(diǎn)繞)
既然 Metaspace 中會(huì)保存類描述信息,可以通過新建類來增加 Metaspace 的占用。
于是想到,使用 CGlib 動(dòng)態(tài)代理,生成被代理類的子類。
簡(jiǎn)單的 SayHello 類。
public?class?SayHello?{public?void?say()?{System.out.println("hello?everyone");} }簡(jiǎn)單的代理類,使用 CGlib 生成子類。
public?class?CglibProxy?implements?MethodInterceptor?{public?Object?getProxy(Class?clazz)?{Enhancer?enhancer?=?new?Enhancer();//?設(shè)置需要?jiǎng)?chuàng)建子類的類enhancer.setSuperclass(clazz);enhancer.setCallback(this);enhancer.setUseCache(false);//?通過字節(jié)碼技術(shù)動(dòng)態(tài)創(chuàng)建子類實(shí)例return?enhancer.create();}//?實(shí)現(xiàn)MethodInterceptor接口方法public?Object?intercept(Object?obj,?Method?method,?Object[]?args,?MethodProxy?proxy)?throws?Throwable?{System.out.println("前置代理");//?通過代理類調(diào)用父類中的方法Object?result?=?proxy.invokeSuper(obj,?args);System.out.println("后置代理");return?result;} }簡(jiǎn)單新建一個(gè) Controller 用于測(cè)試生成 10000 個(gè) SayHello 子類。
@RequestMapping(value?=?"/getProxy",?method?=?RequestMethod.GET) @ResponseBody public?void?getProxy()?{CglibProxy?proxy?=?new?CglibProxy();for?(int?i?=?0;?i?<?10000;?i++)?{//通過生成子類的方式創(chuàng)建代理類SayHello?proxyTmp?=?(SayHello)?proxy.getProxy(SayHello.class);proxyTmp.say();} }應(yīng)用啟動(dòng)完畢后,請(qǐng)求 /getProxy 接口,發(fā)現(xiàn) Metaspace 空間占用上升。
?
CGlib 動(dòng)態(tài)代理生成子類
從堆 Dump 中也可以發(fā)現(xiàn),有很多被 CGlib 所代理的 SayHello 類對(duì)象。
?
堆 Dump 分析
代理類對(duì)應(yīng)的 java.lang.Class 對(duì)象分配在堆內(nèi),類的描述信息在 Metaspace 中。
堆中有多個(gè) Class 對(duì)象,可以推斷出 Metasapce 需要裝下很多類描述信息。
最后,當(dāng) Metaspace 使用空間超過設(shè)置的 -XX:MaxMetaspaceSize=128m 時(shí),就會(huì)發(fā)生 OOM。
1Exception?in?thread?"http-nio-8080-exec-6"?java.lang.OutOfMemoryError:?Metaspace
從 GC log 中可以看到,JVM 會(huì)在 Metaspace 占用滿之后,嘗試 Full GC。
但會(huì)出現(xiàn)以下字樣。
1Full?GC?(Last?ditch?collection)
此外,還有一個(gè)問題。
當(dāng) Metaspace 內(nèi)存占用未達(dá)到 -XX:MetaspaceSize 時(shí),Metaspace 只擴(kuò)容,不會(huì)引起 Full GC。
當(dāng) Metaspace 內(nèi)存占用達(dá)到 -XX:MetaspaceSize 時(shí),會(huì)發(fā)生 Full GC。
在發(fā)生第一次 Full GC 之后,Metaspace 依然會(huì)擴(kuò)容。
那么,第二次觸發(fā) Full GC 的條件是?
有文章說,在觸發(fā)第一次F Full GC 后,之后 Metaspace 的每次擴(kuò)容,都會(huì)引起 Full GC。
但觀察本文測(cè)試的 GC log 和 jstat 命令查看 Metasapce 擴(kuò)容狀況,可以看出:
在第一次 Full GC 之后,之后 Metaspace 的擴(kuò)容,并不一定會(huì)引起 Full GC。
?
觸發(fā)一次 Full GC
從 jstat 輸出可以看到,在觸發(fā)一次 Full GC 之后,Metaspace 依舊發(fā)生了擴(kuò)容,但未發(fā)生 Full GC。
jstat FGC 次數(shù)一直都是 1。
此外,使用 GClib 動(dòng)態(tài)生成類,Metaspace 繼續(xù)擴(kuò)容,到一定程度,觸發(fā)了 Full GC。
但觸發(fā) FGC 時(shí),Metaspace 占比并沒用明顯的規(guī)律。
?
Metaspace 持續(xù)擴(kuò)容再次觸發(fā) FGC
嘗試了幾次,由于 jstat 設(shè)置了 1s 鐘輸出一次,所以每次觸發(fā) Full GC 時(shí)候,MC 的數(shù)據(jù)都不一樣,但基本是相同。
猜測(cè)在第一次 Full GC 之后,之后再次觸發(fā) Full GC 的閾值是有一定的計(jì)算公式的。
但具體如何計(jì)算,估計(jì)是需要深入源碼了。
此外可以看到,每次 Metaspace 擴(kuò)容時(shí),都伴隨著一次 YGC 或者 Full GC,不知道是否是巧合。
接著看到?占小狼?的文章 《JVM源碼分析之垃圾收集的執(zhí)行過程》。
文章有一句話:
從上述分析中可以發(fā)現(xiàn),gc操作的入口都位于GenCollectedHeap::do_collection方法中。
不同的參數(shù)執(zhí)行不同類型的gc。
打開 openjdk 8 中的 GenCollectedHeap 類,查看 do_collection 方法。
可以看到,在 do_collection 方法中,有這個(gè)一段代碼。
if?(complete)?{//?Delete?metaspaces?for?unloaded?class?loaders?and?clean?up?loader_data?graphClassLoaderDataGraph::purge();MetaspaceAux::verify_metrics();//?Resize?the?metaspace?capacity?after?full?collectionsMetaspaceGC::compute_new_size();update_full_collections_completed(); }其中最主要的是?MetaspaceGC::compute_new_size();。
得出,YGC 和 Full GC 的確會(huì)重新計(jì)算 Metaspace 的大小。
至于是否進(jìn)行擴(kuò)容和縮容,則需要根據(jù)?compute_new_size()?方法的計(jì)算結(jié)果而定。
得出,Metasapce 擴(kuò)容導(dǎo)致 GC 這個(gè)說法,其實(shí)是不準(zhǔn)確的。
正確的過程是:新建類導(dǎo)致 Metaspace 容量不夠,觸發(fā) GC,GC 完成后重新計(jì)算 Metaspace 新容量,決定是否對(duì) Metaspace 擴(kuò)容或縮容。
總結(jié)
以上是生活随笔為你收集整理的由「Metaspace容量不足触发CMS GC」从而引发的思考的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PS里最方便的调色工具,但要掌握这个要点
- 下一篇: Mac电脑的睡眠状态如何设置如何设置电脑