事件回顧
清楚的記得是2020/7/25 14:34分左右,周六的下午,我還在公司苦逼的加班中,突然釘釘告警群裡出現大量應用OP的dubbo逾時調用、空指針異常,異常中間還有Metaspace元空間不足等異常:
o.a.c.f.l.ListenerContainer 98 [ERROR] Listener \(org.apache.curator.framework.recipes.cache.PathChildrenCache$3@7edb7fd5) threw an exception
java.lang.OutOfMemoryError: Metaspace
錯誤類型:【oom】
告警内容:2020-07-25 15:05:05:113 d5f54db7c1ca49ab85b9f54cde234bd1 c.c.d.l.w.DriverTraceWriterUtil 39 [ERROR] driver trace writer to file fail,ex:[{}]
java.lang.RuntimeException: by java.lang.ClassFormatError: Metaspace
at com.caocao.dc
複制
再緊接着,發現我們應用OP的伺服器大量FullGC,先一台發生,很快第二台開始FGC,第10台...
2020-07-25T15:10:50
應用:xxx
主機:xxx(
agentId: yyyy
發生FGC,共耗時:25012ms
2020-07-25T15:10:25
應用:xxxx
主機:xxxx
agentId: yyy
發生FGC,共耗時:4223ms
複制
涉及到對OP系統調用的各系統都在回報出現dubbo調用逾時,都在報錯中,我們通過pinpoint也發現應用頻繁發生了FGC:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICMyYTMvw1dvwlMvwlM3VWaWV2Zh1Wa-cmbw5CcnVWOuhjeuZzcvwVMycTNyczMtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
1.png
2.png
上面我們大概可以判斷出來,是由于Metaspace元空間不足,出現記憶體溢出,導緻jvm頻繁觸發full GC,為了保證業務正常,此時我們讓運維緊急重新開機了伺服器,通過重新開機伺服器,業務逐漸恢複正常,元空間使用量也降下來了。在發生FGC時讓運維dump記憶體了,後面會分析該檔案。
JVM參數
OP應用的生成JVM參數如下:
/usr/local/java/bin/java
-server #指定JVM的啟動模式是client模式還是server模式
-Xms4g #初始化堆記憶體4G,堆記憶體最小值
-Xmx4g #最大堆4g
-Xmn2g #年輕代2G,老年代大小=Xmx-Xmn
-Xss512k #每個線程的堆棧大小
-XX:MetaspaceSize=256m #元空間的初始大小
-XX:MaxMetaspaceSize=512m #元空間最大值
-XX:-UseGCOverheadLimit #預測是否要OOM了,提前抛出異常,防止OOM發生
-XX:+DisableExplicitGC #禁用System.gc()
-XX:+UseConcMarkSweepGC #指定老年代的收集算法使用CMS,會預設使用ParNew作為新生代收集器
-XX:+CMSParallelRemarkEnabled #開啟并行标記,減少停頓時間
-XX:+UseCMSCompactAtFullCollection #FULL GC時對老年代進行壓縮。CMS預設不會移動記憶體,是以容易産生碎片。增加該參數雖然會影響性能,但可以消除碎片
-XX:+UseFastAccessorMethods #正确擷取方法的調用計數,以便VM可以更好地識别代碼中的熱點
-XX:+UseCMSInitiatingOccupancyOnly #指定HotSpot VM總是使用-XX:CMSInitiatingOccupancyFraction的值作為老年代使用率限制來啟動CMS垃圾回收。如果沒有使用-XX:+UseCMSInitiatingOccupancyOnly,那麼HotSpot VM隻是利用CMSInitiatingOccupancyFraction啟s動第一次CMS垃圾回收,後面都是使用HotSpot VM自動計算出來的值
-XX:CMSInitiatingOccupancyFraction=70 #CMS垃圾收集器,老年代使用率達到70%時,觸發CMS垃圾回收
-XX:LargePageSizeInBytes=128m #堆記憶體大頁的大小,大的記憶體分頁可以增強 CPU 的記憶體尋址能力,進而提升系統的性能
-Djava.awt.headless=true
-Djava.net.preferIPv4Stack=true
-Ddubbo.application.qos.port=12881
-javaagent:/usr/local/pinpoint/pinpoint-bootstrap-1.6.0.jar
-Dpinpoint.agentId=driver-op-...
-Dpinpoint.applicationName=OP
-Djava.ext.dirs=/usr/local/springboot/OP/lib:/usr/local/java/jre/lib/ext
-XX:+HeapDumpOnOutOfMemoryError #當堆記憶體空間溢出時輸出堆的記憶體快照,配合-XX:HeapDumpPath使用
-XX:HeapDumpPath=/home/admin #當堆記憶體空間溢出時輸出堆的記憶體快照輸出目錄
-cp /usr/local/springboot/OP/conf:.:/usr/local/java/lib:/usr/local/java/jre/lib -jar /usr/local/springboot/OP/OP.jar
複制
由配置的JVM參數知道,指定了CMS為老年代的垃圾收集器,預設ParNew為新生代垃圾收集器,最大堆4g,老年代2g,年輕代2g,年輕中Eden區域和Survivor區域(From幸存區或To幸存區)的預設比例為8, 即設定survivor:eden=2:8(From:TO:eden=200MB:200MB:1600MB),元空間初始化大小256MB,最大值512MB,如果老年代空間使用率達到70%,會觸發CMS垃圾回收。由pinpoint上可以看出,元空間使用大概在770MB左右,超過了最大元空間值,導緻元空間記憶體不足,觸發FGC,這裡有個疑問,明明配置的最大512MB,為什麼使用了770MB,Metaspace還有一個區間是Klass Metaspace,由參數-XX:CompressedClassSpaceSize進行控制,JDK8的時候 Klass Metaspace預設是1G。
原因分析
-
MAT分析
使用MAT打開dump檔案,點開
柱狀圖,選擇Histogram
,右擊選擇java.lang.Class
,選擇List objects
,檢視通過這個class建立的類資訊:with incoming references(目前檢視的對象,被外部引用)
發現建立了大量
Proxy
類,右擊選中
Path To GC Roots
,選中
exclude all phantom/weak/soft etc.references
(排除虛引用/弱引用/軟引用等的引用鍊,被虛引用/弱引用/軟引用的對象可以直接被GC給回收,要看該對象否還存在Strong引用鍊,如果有,則說明存在記憶體洩漏):
發現
Proxy
類被
org.springframework.boot.loader.LaunchedURLClassLoader
強引用,導緻生成的
Proxy
類無法被解除安裝一直殘留在MetaSpace區造成記憶體洩漏。
-
代碼分析
上面分析出來生成
類可能存在記憶體洩漏,代碼中會發現用動态代理建立Proxy
類對象并放入WeakReference中,每次GC時該對象都會被回收,會重複建立Proxy
類對象,而且類加載器不會被回收,導緻類不會被解除安裝。具體代碼參考了dubbo代碼Proxy
。com.alibaba.dubbo.common.bytecode.Proxy#getProxy(java.lang.ClassLoader, java.lang.Class<?>...)
-
解決方法
上層業務做緩存處理,不會重複建立
對象。上線觀察優化前後5天内的元空間增長,的确效果比較明顯。Proxy
參考文章
- https://www.jianshu.com/p/738b4f3bc44b
- https://www.cnblogs.com/throwable/p/12216546.html
- https://blog.csdn.net/a15939557197/article/details/90635460?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param