標籤:safe lin this should static 項目 wsdl 問題 overwrite
多圖預警!
- 環境:系統測試(Windows Server/JRE8/tomcat7)
- 現象:應用運行幾天后,出現訪問逾時,伺服器cpu利用率居高不下
- 問題日誌:OutOfMemoryError:MetaSpace
- 問題分析:
- 原因分析:MetaSpace是jvm存放類資訊的記憶體空間,發生溢出的可能原因:
- metaSpace設定過小,不足應用所需
- 應用metaSpace持續增長,超過metaSpace限制
- 定位:問題最先從DeviceStatusMonitorTask中報出,而這個定時任務新版本修改了同步裝置狀態的功能,主要是與vag通訊擷取裝置狀態資訊。
- 猜測:
- 裝置狀態監控任務中動態組建代理程式類,導致metaSpace不斷消耗
- 重現:
- 本地運行應用,添加多個可用裝置,縮短task執行間隔
- 開啟Java VisualVM監控
- 限制Metaspace最大值:-XX:MaxMetaspaceSize=100m
從JVisualVM的監控視圖中,我們可以直觀的看出每隔一分鐘都會出現線程數飆升、類載入數階梯式增長的情況。
隨著類載入數的增長,Metaspace空間逐步從60M增長到100M,出現記憶體溢出,導致jvm頻繁觸發full GC,消耗大量CPU資源。
Task類 run方法代碼:
紅框部分為新增代碼,具體實現如下:
主要邏輯是與底層組件通訊查詢運行狀態,然後根據結果更新狀態。直接排除DAO操作的嫌疑,抽取與通訊部分,整理成單獨的測試代碼:
步驟: 1. 設定jvm參數 : -verbose:class 列印類載入資訊
2. 清理控制台日誌,調試代碼。
調試過程中,我發現每次迴圈都會有新的類被載入:
而這些類都是在下面這行代碼運行之後載入的。
結合類載入資訊以及sendRequest方法的實現,基本確認問題是由JaxbUtil處理xml、JavaBean的相互轉換引起。
繼續調試分析,發現JAXBContext對象初始化時會動態載入class,而JaxbUtil每次調用都會重新建立一個JAXBContext。
問題根因既已找到,解決思路自然清晰明確。
考慮到jdk中已有JAXB工具類提供xml和javaBean的互轉,借鑒源碼發現JAXB使用弱引用Cache對象來緩衝JAXBContext。
/** * Cache. We don‘t want to prevent the {@link Cache#type} from GC-ed, * hence {@link WeakReference}. */ private static volatile WeakReference<Cache> cache; /** * Obtains the {@link JAXBContext} from the given type, * by using the cache if possible. * * <p> * We don‘t use locks to control access to {@link #cache}, but this code * should be thread-safe thanks to the immutable {@link Cache} and {@code volatile}. */ private static <T> JAXBContext getContext(Class<T> type) throws JAXBException { WeakReference<Cache> c = cache; if(c!=null) { Cache d = c.get(); if(d!=null && d.type==type) return d.context; } // overwrite the cache Cache d = new Cache(type); cache = new WeakReference<Cache>(d); return d.context; }
結合應用的實際情境,上面的實現避免了短時間頻繁建立JAXBContext。但是弱引用Cache在無引用的情況下會很快被GC回收,所以每次定時任務都會重建context;並且Cache對象只能儲存一個context,在定時任務的運行過程中可能由於其他介面通訊導致context切換。綜上,JAXB的實現也無法滿足當前應用的需要。
沒有現成的解決方案,只好自己寫一個。
由建立JAXBContext引起問題,那就延長對象的生命週期,減少建立對象。對於相同的Class,可以使用同一個context對象與xml互相轉換。由於vag的介面個數有限, 其xml報文格式並不多,因此,維護一個static Map<Class<?>, JAXBContext>來儲存context對象佔用的記憶體並不多。考慮到與vag通訊屬於並發執行,使用ConcurrentHashMap實現保證並發安全。
最終代碼如下:
將之前的測試代碼類比定時任務略微修改,每隔10s執行一次,重複50次。
開啟JVisualVM監視視圖,可以明確的看出類裝載數在第一次迴圈時就已接近最大值,後續過程中只載入了極少數量的class,證明這種方案確實可行。
使用修改後的代碼運行整個項目,16小時後的監視映像顯示:類載入數保持穩定,MetaSpace大小几乎無變化。
- 總結
- 排查問題的思路適用於一般的jvm永久代或元空間溢出。
- 主要採用單例模式的思想解決建立大量複雜物件引起的資源消耗問題。
另外,前段時間還使用-verbose:class 參數排查出Apache CXF產生的webservice用戶端重複初始化引起的OOM問題的原因。用戶端初始化的過程中也會根據wsdl檔案動態產生class並載入,因此,使用CXF用戶端代碼時,應盡量使用單例模式。
記一次JVM Metaspace溢出排查