Java服務假死後續之記憶體溢位

2022-07-05 09:00:27

一、現象分析

  上篇部落格說到,Java服務假死的原因是使用了Guava快取,30分鐘的有效期導致Full GC無法回收記憶體。經過優化後,已經不再使用Guava快取,實時查詢資料。從短期效果來看,確實解決了無法回收記憶體的問題,但是服務執行幾天後,發現記憶體又逐漸被佔滿,Full GC後只能回收一小部分。

從上圖可以看出,一次Full GC後,老年代基本上沒有回收多少記憶體,佔比從99.86%降到99.70%。

二、原因排查

   到底是什麼物件佔據這麼大的記憶體,並且無法被JVM垃圾回收呢。在上一篇部落格中已經移除了Guava快取,按理說不應該有無法回收的物件了。那麼,很明顯這應該是程式碼問題導致了記憶體洩露,現在需要知道哪些物件無法被回收,從而定位出程式碼哪裡有BUG。這裡採用jmap -histo:live 201349|head -10命令列印出GC後存活的物件。

  從上圖可以看出,還是之前存在Guava快取裡面的物件佔據著大部分記憶體,程式碼修改為實時查詢後,每次用完資料都會從Map中剔除,按理不應該有強參照去參照這些物件。光看程式碼無法排查出哪裡導致了記憶體洩露,只能將GC後的記憶體檔案匯出來進行分析。這裡採用jmap -dump:format=b,file=/data/heap.hprof命令將記憶體檔案匯出來,用JDK自帶的visualVM開啟。

  這裡拿ECBug物件進行分析,從參照關係可以看出,ECBug物件被DataSetCenter參照,DataSetCenter就是實時查詢資料進行儲存的一個ConcurrentHashMap,但每次用完資料後都會進行remove操作,具體程式碼如下所示。

private List<BusinessBean> realTimeQueryBusinessModelData(IDataSetKey accessCacheDataSetKey,Set<IMapper> mappers, Set<IFilter> filters, Set<ISorter> sorters) throws DataNotFoundException, IllegalAccessException, CloneNotSupportedException, InstantiationException {
        List<BusinessBean> resultBeans = null;
        try {
            lock.lock();
            if (!dataSetCenter.containsKey(accessCacheDataSetKey)) {
                log.info("put DataSetKey into DataSetCenter,dataSetKey is {}",accessCacheDataSetKey);
                int count = businessModelQuery.count(accessCacheDataSetKey);
                if (count == 0) throw new DataNotFoundException();
                Class modelClass = businessModelCenter.getDataModelClass(accessCacheDataSetKey.getModelId());
                if (modelClass == null) {
                    throw new DataNotFoundException();
                }
                dataSetCenter.put(accessCacheDataSetKey, new DataSet(count, modelClass));
            }
            List<BusinessBean> cachedBeans = dataSetCenter.get(accessCacheDataSetKey).getData();
            resultBeans =  getModelDataInternal(accessCacheDataSetKey, businessModelQuery, mappers, filters, sorters, cachedBeans);
        }finally {
            lock.unlock();
            if(!lock.isLocked()){
                dataSetCenter.remove(accessCacheDataSetKey);
            }
        }
        return resultBeans;
    }

  從程式碼來看,每次 dataSetCenter.put(accessCacheDataSetKey, new DataSet(count, modelClass))後,都會在finally裡面呼叫dataSetCenter.remove(accessCacheDataSetKey)把key刪除掉,這樣在GC時會自動回收Value值。但是忽略了一個方法getModelDataInternal,該方法可能會遞迴呼叫realTimeQueryBusinessModelData方法,如果存在遞迴呼叫的話,那麼由於可重入鎖lock還沒有完成解鎖,所以無法進入if(!lock.isLocked())條件語句中進行刪除key的操作,這樣就造成了一部分資料無法被刪除,隨著時間的推移,記憶體中的資料會越來越多。

三、故障解決

   基於上述的程式碼分析,改造如下所示。

private List<BusinessBean> realTimeQueryBusinessModelData(IDataSetKey accessCacheDataSetKey,Set<IMapper> mappers, Set<IFilter> filters, Set<ISorter> sorters) throws DataNotFoundException, IllegalAccessException, CloneNotSupportedException, InstantiationException {
        List<BusinessBean> resultBeans = null;
        try {
            queryLock.lock();
            modelQueryLock.lock();
            if (!dataSetCenter.containsKey(accessCacheDataSetKey)) {
                log.info("put DataSetKey into DataSetCenter,dataSetKey is {}",accessCacheDataSetKey);
                int count = businessModelQuery.count(accessCacheDataSetKey);
                if (count == 0) throw new DataNotFoundException();
                Class modelClass = businessModelCenter.getDataModelClass(accessCacheDataSetKey.getModelId());
                if (modelClass == null) {
                    throw new DataNotFoundException();
                }
                dataSetCenter.put(accessCacheDataSetKey, new DataSet(count, modelClass));
            }
            List<BusinessBean> cachedBeans = dataSetCenter.get(accessCacheDataSetKey).getData();
            resultBeans =  getModelDataInternal(accessCacheDataSetKey, businessModelQuery, mappers, filters, sorters, cachedBeans);
        }finally {
            modelQueryLock.unlock();
            if(!modelQueryLock.isLocked()){
                removeDataSetKeys();
            }
            queryLock.unlock();
        }
        return resultBeans;
    }

   這裡當modelQueryLock可重入鎖完全解鎖後,呼叫removeDataSetKeys方法,該方法會將dataSetCenter裡面的key全部刪除,這樣在GC時就會回收不用的資料物件。這裡採用兩個可重入鎖的目的是,如果只用一個modelQueryLock可重入鎖,那麼當modelQueryLock完全解鎖後,正在執行removeDataSetKeys方法時,其他執行緒就可以進入該方法區,發現dataSetCenter裡面還沒有刪除完全,從而獲取裡面的資料,即if (!dataSetCenter.containsKey(accessCacheDataSetKey))為false,從而通過List<BusinessBean> cachedBeans = dataSetCenter.get(accessCacheDataSetKey).getData()直接獲取dataSetCenter裡面的資料,但是下一刻dataSetCenter裡面可能已經為空。因此,採用兩個可重入鎖,防止出現異常。

 

作者:kbkb

本文為作者原創,轉載請註明出處:https://www.cnblogs.com/kbkb/p/16442886.html