1. 程式人生 > >記一次JVM Metaspace溢出排查

記一次JVM Metaspace溢出排查

jvm參數 oom task visualvm map 排除 創建 thread 類裝載

多圖預警!

  • 環境:系統測試(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溢出排查