轉載請註明原文連接: https://www.jianshu.com/p/468...
某天早上,毛老師在羣裏問「cat 上怎麼看 gc」。java
看到有 GC 的問題,立馬作出小雞搓手狀。算法
以後毛老師發來一張圖。app
圖片展現了老年代內存佔用狀況。工具
第一個大陡坡是應用發佈,老年代內存佔比降低,很正常。源碼分析
第二個小陡坡,老年代內存佔用忽然降低,應該是發生了老年代 GC。測試
但奇怪的是,此時老年代內存佔用並不高,發生 GC 並非正常現象。this
因而,毛老師查看了 GC log。spa
從 GC log 中能夠看出,老年代發生了一次 CMS GC。.net
但此時老年代內存使用佔比 = 234011K / 2621440k ≈ 9%。代理
而 CMS 觸發的條件是:
老年代內存使用佔比達到 CMSInitiatingOccupancyFraction,默認爲 92%,
毛老師設置的是 75%。
-XX:CMSInitiatingOccupancyFraction = 75
因而排除老年代佔用太高的可能。
接着分析內存情況。
毛老師發如今老年代發生 GC 時,Metaspace 的內存佔用也一塊兒降低。
因而懷疑是 Metaspace 佔用達到了設置的參數 MetaspaceSize,發生了 GC。
查看 JVM 參數設置,MetaspaceSize 參數被設置爲128m。
-XX:MetaspaceSize = 128m -XX:MaxMetaspaceSize = 256m
問題的緣由被集中在 Metaspace 上。
毛老師查看另一個監控工具,發生小陡坡的縱座標的確接近 128m。
此時,引起出另外一個問題:
Metaspace 發生 GC,爲什麼會引發老年代 GC。
因而,想到以前看過 阿飛Javaer 的文章 《JVM參數MetaspaceSize的誤解》。
其中有幾個關鍵點:
Metaspace 在空間不足時,會進行擴容,並逐漸達到設置的 MetaspaceSize。Metaspace 擴容到 -XX:MetaspaceSize 參數指定的量,就會發生 FGC。
若是配置了 -XX:MetaspaceSize,那麼觸發 FGC 的閾值就是配置的值。
若是 Old 區配置 CMS 垃圾回收,那麼擴容引發的 FGC 也會使用 CMS 算法進行回收。
其中的關鍵點是:
若是老年代設置了 CMS,則 Metasapce 擴容引發的 FGC 會轉變成一次 CMS。
查看毛老師配置的 JVM 參數,果真設置了 CMS GC。
-XX:+UseConcMarkSweepGC
因而,解決問題的方法是調整 -XX:MetaspaceSize = 256m。
從監控來看,設置 -XX:MaxMetaspaceSize = 256m 已經足夠。
由於後期並不會引起 CMS GC。
GC 的問題算是解決了,但同時引起了如下幾點思考:
關於這個問題一和問題二,阿飛Javaer 已經解釋的比較清楚。
對於 Metaspce,其初始大小並不等於設置的 -XX:MetaspaceSize 參數。
隨着類的加載,Metaspce 會不斷進行擴容,直到達到 -XX:MetaspaceSize 觸發 GC。
而至於如何設置 Metaspace 的初始大小,目前的確沒有辦法。
在 openjdk 的 bug 列表中,找到一個 關於 Metaspace 初始大小的 bug,而且還沒有解決。
對於問題二, 阿飛Javaer 在文章中也進行了說明。
Perm 的話,咱們經過配置 -XX:PermSize 以及 -XX:MaxPermSize 來控制這塊內存的大小。JVM 在啓動的時候會根據 -XX:PermSize 初始化分配一塊連續的內存塊。
這樣的話,若是 -XX:PermSize 設置過大,就是一種赤果果的浪費。
關於 Metaspace,JVM 還提供了其他一些設置參數。
能夠經過如下命令查看。
java -XX:+PrintFlagsFinal -version | grep Metaspace
關於 Metaspace 更多的內容,能夠參考笨神的文章:《JVM源碼分析之Metaspace解密》。
問題三
Metaspace 佔用到達 -XX:MetaspaceSize 會引起什麼?
已經知道,當老年代回收設置成 CMS GC 時,會觸發一次 CMS GC。
那麼若是不設置爲 CMS GC,又會發生什麼呢?
使用如下配置進行一個小嚐試,而後查看 GC log。
-Xmx2048m -Xms2048m -Xmn1024m -XX:MetaspaceSize=40m -XX:MaxMetaspaceSize=128m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:d:/heap_trace.txt
該配置並未設置 CMS GC,JDK 1.8 默認的老年代回收算法爲 ParOldGen。
本文測試的應用在啓動完成後,佔用 Metaspace 空間約爲 63m,可經過 jstat 命令查看。
因而,設置 -XX:MetaspaceSize = 40m,指望發生一次 GC。
從 GC log 中,能夠找到如下關鍵日誌。
[GC (Metadata GC Threshold) [PSYoungGen: 360403K->47455K(917504K)] 360531K->47591K(1966080K), 0.0343563 secs] [Times: user=0.08 sys=0.00, real=0.04 secs] [Full GC (Metadata GC Threshold) [PSYoungGen: 47455K->0K(917504K)] [ParOldGen: 136K->46676K(1048576K)] 47591K->46676K(1966080K), [Metaspace: 40381K->40381K(1085440K)], 0.1712704 secs] [Times: user=0.42 sys=0.02, real=0.17 secs]
能夠看出,因爲 Metasapce 到達 -XX:MetaspaceSize = 40m 時候,觸發了一次 YGC 和一次 Full GC。
通常而言,咱們對 Full GC 的重視度比對 YGC 高不少。
因此通常都會直描述,當 Metasapce 到達 -XX:MetaspaceSize 時會觸發一次 Full GC。
問題四
如何人工模擬 Metaspace 內存佔用上升?
Metaspace 是 JDK 1.8 以後引入的一個區域。
有一點能夠確定的,Metaspace 會保存類的描述信息。
JVM 須要根據 Metaspace 中的信息,才能找到堆中類 java.lang.Class 所對應的對象。(有點繞)
既然 Metaspace 中會保存類描述信息,能夠經過新建類來增長 Metaspace 的佔用。
因而想到,使用 CGlib 動態代理,生成被代理類的子類。
簡單的 SayHello 類。
public class SayHello { public void say() { System.out.println("hello everyone"); } }
簡單的代理類,使用 CGlib 生成子類。
public class CglibProxy implements MethodInterceptor { public Object getProxy(Class clazz) { Enhancer enhancer = new Enhancer(); // 設置須要建立子類的類 enhancer.setSuperclass(clazz); enhancer.setCallback(this); enhancer.setUseCache(false); // 經過字節碼技術動態建立子類實例 return enhancer.create(); } // 實現MethodInterceptor接口方法 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("前置代理"); // 經過代理類調用父類中的方法 Object result = proxy.invokeSuper(obj, args); System.out.println("後置代理"); return result; } }
簡單新建一個 Controller 用於測試生成 10000 個 SayHello 子類。
@RequestMapping(value = "/getProxy", method = RequestMethod.GET) @ResponseBody public void getProxy() { CglibProxy proxy = new CglibProxy(); for (int i = 0; i < 10000; i++) { //經過生成子類的方式建立代理類 SayHello proxyTmp = (SayHello) proxy.getProxy(SayHello.class); proxyTmp.say(); } }
應用啓動完畢後,請求 /getProxy 接口,發現 Metaspace 空間佔用上升。
從堆 Dump 中也能夠發現,有不少被 CGlib 所代理的 SayHello 類對象。
代理類對應的 java.lang.Class 對象分配在堆內,類的描述信息在 Metaspace 中。
堆中有多個 Class 對象,能夠推斷出 Metasapce 須要裝下不少類描述信息。
最後,當 Metaspace 使用空間超過設置的 -XX:MaxMetaspaceSize=128m 時,就會發生 OOM。
Exception in thread "http-nio-8080-exec-6" java.lang.OutOfMemoryError: Metaspace
從 GC log 中能夠看到,JVM 會在 Metaspace 佔用滿以後,嘗試 Full GC。
但會出現如下字樣。
Full GC (Last ditch collection)
此外,還有一個問題。
當 Metaspace 內存佔用未達到 -XX:MetaspaceSize 時,Metaspace 只擴容,不會引發 Full GC。
當 Metaspace 內存佔用達到 -XX:MetaspaceSize 時,會發生 Full GC。
在發生第一次 Full GC 以後,Metaspace 依然會擴容。
那麼,第二次觸發 Full GC 的條件是?
有文章說,在觸發第一次F Full GC 後,以後 Metaspace 的每次擴容,都會引發 Full GC。
但觀察本文測試的 GC log 和 jstat 命令查看 Metasapce 擴容情況,能夠看出:
在第一次 Full GC 以後,以後 Metaspace 的擴容,並不必定會引發 Full GC。
從 jstat 輸出能夠看到,在觸發一次 Full GC 以後,Metaspace 依舊發生了擴容,但未發生 Full GC。
jstat FGC 次數一直都是 1。
此外,使用 GClib 動態生成類,Metaspace 繼續擴容,到必定程度,觸發了 Full GC。
但觸發 FGC 時,Metaspace 佔比並沒用明顯的規律。
嘗試了幾回,因爲 jstat 設置了 1s 鍾輸出一次,因此每次觸發 Full GC 時候,MC 的數據都不同,但基本是相同。
猜想在第一次 Full GC 以後,以後再次觸發 Full GC 的閾值是有必定的計算公式的。
但具體如何計算,估計是須要深刻源碼了。
此外能夠看到,每次 Metaspace 擴容時,都伴隨着一次 YGC 或者 Full GC,不知道是不是巧合。
接着看到 佔小狼 的文章 《JVM源碼分析之垃圾收集的執行過程》。
文章有一句話:
從上述分析中能夠發現,gc操做的入口都位於GenCollectedHeap::do_collection方法中。
不一樣的參數執行不一樣類型的gc。
打開 openjdk 8 中的 GenCollectedHeap 類,查看 do_collection 方法。
能夠看到,在 do_collection 方法中,有這個一段代碼。
if (complete) { // Delete metaspaces for unloaded class loaders and clean up loader_data graph ClassLoaderDataGraph::purge(); MetaspaceAux::verify_metrics(); // Resize the metaspace capacity after full collections MetaspaceGC::compute_new_size(); update_full_collections_completed(); }
其中最主要的是 MetaspaceGC::compute_new_size();
。
得出,YGC 和 Full GC 的確會從新計算 Metaspace 的大小。
至因而否進行擴容和縮容,則須要根據 compute_new_size()
方法的計算結果而定。
得出,Metasapce 擴容致使 GC 這個說法,實際上是不許確的。
正確的過程是:新建類致使 Metaspace 容量不夠,觸發 GC,GC 完成後從新計算 Metaspace 新容量,決定是否對 Metaspace 擴容或縮容。