一、現象分析
上篇部落格說到,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