一. 現象
前段時間公司線上環境的一個Java應用因為OOM的異常報警,導緻整個服務不可用被拉出叢集,本地模拟重制的現象如下:

當時的解決方案是增加metaspace的容量:-XX:MaxMetaspaceSize=500m,從原來預設的256m改為500m,雖然沒有再出現oom,但這個隻是臨時解決方案,通過公司的監控系統觀察metaspace的使用情況還是在上升,而且後面随着業務通路量越來越大還是有可能達到門檻值。
二. 分析
Metaspace元空間主要是存儲類的中繼資料資訊,我們的應用裡加載的各種類描述資訊,比如類名、屬性、方法、通路限制等,按照一定的結構存儲在Metaspace裡。
由此可知metaspace空間增長是由于反射類加載,動态代理生成的類加載等導緻的,也就是說Metaspace的大小和加載類的資料有關系,加載的類越多metaspace占用的記憶體也就越大。
因為了解當時的業務場景是因為有個郵件服務通路訂單詳情接口的通路量突然上升,以及檢視log的eroor日志發現大部分都是訂單詳情接口先報出的這個問題:java.lang.OutOfMemoryError: Metaspace
這裡我在測試環境Java應用的jvm裡增加-XX:+TraceClassLoading -XX:+TraceClassUnloading記錄下類的加載和解除安裝情況,然後通過jmeter多個線程調用訂單詳情接口模拟metaspace溢出的現象,發現在catalina.out檔案裡輸出的除了業務上用到的類外還有大量的反射類,如下:
這些反射類被頻繁的加載和解除安裝是不正常的,通過Arthas診斷工具(Java線上診斷利器之Arthas)觀察調用鍊發現每次調用接口都是通過反射的方式實作的。
目前我們的項目都是基于SOA架構對外提供通路的,從上圖sun.reflect的調用者也能看出來
通過上圖可以看出在調用底層接口時都是通過反射的方式擷取類的執行個體,檢視架構底層代碼實作可以确認
同樣對底層接口傳回的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這種幫你生成set、get方法的工具,效果會更好。
END -