JVM Metaspace內存溢出排查與總結

一. 現象

前段時間公司線上環境的一個Java應用由於OOM的異常報警,致使整個服務不可用被拉出集羣,本地模擬重現的現象以下:html

當時的解決方案是增長metaspace的容量:-XX:MaxMetaspaceSize=500m,從原來默認的256m改成500m,雖然沒有再出現oom,但這個只是臨時解決方案,經過公司的監控系統觀察metaspace的使用狀況仍是在上升,並且後面隨着業務訪問量愈來愈大仍是有可能達到閾值。java

二. 分析

Metaspace元空間主要是存儲類的元數據信息,咱們的應用里加載的各類類描述信息,好比類名、屬性、方法、訪問限制等,按照必定的結構存儲在Metaspace裏。apache

由此可知metaspace空間增加是因爲反射類加載,動態代理生成的類加載等致使的,也就是說Metaspace的大小和加載類的數據有關係,加載的類越多metaspace佔用的內存也就越大。json

由於瞭解當時的業務場景是由於有個郵件服務訪問訂單詳情接口的訪問量忽然上升,以及查看log的eroor日誌發現大部分都是訂單詳情接口先報出的這個問題:java.lang.OutOfMemoryError: Metaspace緩存

這裏我在測試環境Java應用的jvm裏增長-XX:+TraceClassLoading -XX:+TraceClassUnloading記錄下類的加載和卸載狀況,而後經過jmeter多個線程調用訂單詳情接口模擬metaspace溢出的現象,發如今catalina.out文件裏輸出的除了業務上用到的類外還有大量的反射類,以下:併發

這些反射類被頻繁的加載和卸載是不正常的,經過Arthas診斷工具(Java在線診斷利器之Arthas)觀察調用鏈發現每次調用接口都是經過反射的方式實現的。框架

目前咱們的項目都是基於SOA框架對外提供訪問的,從上圖sun.reflect的調用者也能看出來jvm

經過上圖能夠看出在調用底層接口時都是經過反射的方式獲取類的實例,查看框架底層代碼實現能夠確認函數

一樣對底層接口返回的json數據反序列化時也會用到反射高併發

繼續跟代碼能夠看到這些反射的實現都會用到java.lang.Class裏的ReflectionData對象

ReflectionData是個內部靜態類被緩存起來,裏面的屬性就是咱們作反射操做時須要用的屬性Field,方法Method和構造函數等。可是有個問題reflectionData是被SoftReference軟引用修飾的,以下圖

若是是軟引用的話在內存空間不足時就可能會被回收掉,若是回收掉那下次再使用的話只能從新經過反射獲取。

而SoftReference是否被回收又跟SoftRefLRUPolicyMSPerMB參數的值有關係,查看咱們線上JVM的配置發現XX:SoftRefLRUPolicyMSPerMB這個參數設置的是0

SoftRefLRUPolicyMSPerMB這個參數大概意思是每1M空閒空間可保持的SoftReference對象的生存時長(單位是ms毫秒),LRU是Least Recently Used的縮寫,最近最少使用的。

這個值jvm默認是1000ms,若是被設置爲0,就會致使軟引用對象立刻被回收掉,進而會致使從新頻繁的生成新的類,而沒法達到複用的效果。

上圖裏大量的sun.reflect.GeneratedSerializationConstructorAccessor,GeneratedMethodAccessor就是這樣產生的。

我把這個參數改回默認值-XX:SoftRefLRUPolicyMSPerMB=1000 (1秒),發佈到生產環境驗證了下,發佈後就降下來了,到今天爲止基本上趨於穩定

調整後基本上沒有再出現波動

三. 總結

  1. 目前主要是經過修改JVM的-XX:SoftRefLRUPolicyMSPerMB值來解決metaspace上升問題,後續會持續觀察變化,適當調整參數。至於這個參數以前爲何會被設置成0, 還須要找ops確認下。
  2. 咱們的應用須要大量RPC交互,屬於I/O密集型業務,使用SOA,Dubbo都會遇到相似的問題,經過上面的源碼分析能夠看出這個是沒法避免的(除非是換一種序列化協議,好比hessian,不走方法反射的方式來賦值)包括自己使用的Spring框架不少地方也是經過反射實現的好比AOP,還有咱們埋點常用的JsonUtils工具,經過dump文件也能看出來存在大量的屬性拷貝和反射操做。

因此咱們在平時的業務代碼開發中若是遇到兩個對象賦值的操做盡可能少用反射的方式實現,好比下面的代碼:

這裏作的對象拷貝操做使用的是apache common-beanutils.jar中的BeanUtils,這個類底層採用javabeans+反射實現,性能比較差,內存開銷比較大,當系統高併發的狀況容易致使Metaspace空間增加過快,不建議這樣使用。

若是字段少的話直接賦值就好了,多的話可使用Cglib的BeanCopier類,BeanCopier類底層是採用asm字節碼操做方式來進行對象拷貝操做,性能損耗和內存開銷都比較小。

或者使用MapStruct這種幫你生成setget方法的工具,效果會更好。

文章來源:javakk.com/160.html

相關文章
相關標籤/搜索