一. 現象
前段時間公司線上環境的一個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秒),發布到生產環境驗證了下,發布后就降下來了,到今天為止基本上趨於穩定
調整后基本上沒有再出現波動
三. 總結
- 目前主要是通過修改JVM的
-XX:SoftRefLRUPolicyMSPerMB
值來解決metaspace上升問題,后續會持續觀察變化,適當調整參數。至於這個參數之前為什么會被設置成0, 還需要找ops確認下。 - 我們的應用需要大量RPC交互,屬於I/O密集型業務,使用SOA,Dubbo都會遇到類似的問題,通過上面的源碼分析可以看出這個是無法避免的(除非是換一種序列化協議,比如
hessian
,不走方法反射的方式來賦值)包括本身使用的Spring框架很多地方也是通過反射實現的比如AOP,還有我們埋點經常使用的JsonUtils
工具,通過dump文件也能看出來存在大量的屬性拷貝和反射操作。
所以我們在平時的業務代碼開發中如果遇到兩個對象賦值的操作盡量少用反射的方式實現,比如下面的代碼:
這里做的對象拷貝操作使用的是apache common-beanutils.jar中的BeanUtils
,這個類底層采用javabeans+反射實現,性能比較差,內存開銷比較大,當系統高並發的情況容易導致Metaspace空間增長過快,不建議這樣使用。
如果字段少的話直接賦值就行了,多的話可以使用Cglib的BeanCopier
類,BeanCopier
類底層是采用asm字節碼操作方式來進行對象拷貝操作,性能損耗和內存開銷都比較小。
或者使用MapStruct這種幫你生成set
、get
方法的工具,效果會更好。