Java服務假死后續(xù)之內存溢出的原因分析
一、現象分析
上篇博客說到,Java服務假死的原因是使用了Guava緩存,30分鐘的有效期導致Full GC無法回收內存。經過優(yōu)化后,已經不再使用Guava緩存,實時查詢數據。從短期效果來看,確實解決了無法回收內存的問題,但是服務運行幾天后,發(fā)現內存又逐漸被占滿,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完全解鎖后,正在執(zhí)行removeDataSetKeys方法時,其他線程就可以進入該方法區(qū),發(fā)現dataSetCenter里面還沒有刪除完全,從而獲取里面的數據,即if (!dataSetCenter.containsKey(accessCacheDataSetKey))為false,從而通過List<BusinessBean> cachedBeans = dataSetCenter.get(accessCacheDataSetKey).getData()直接獲取dataSetCenter里面的數據,但是下一刻dataSetCenter里面可能已經為空。因此,采用兩個可重入鎖,防止出現異常。
到此這篇關于Java服務假死后續(xù)之內存溢出的文章就介紹到這了,更多相關Java 內存溢出內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
簡述springboot及springboot cloud環(huán)境搭建
這篇文章主要介紹了簡述springboot及springboot cloud環(huán)境搭建的方法,包括spring boot 基礎應用環(huán)境搭建,需要的朋友可以參考下2017-07-07
Shiro + JWT + SpringBoot應用示例代碼詳解
這篇文章主要介紹了Shiro (Shiro + JWT + SpringBoot應用),本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06
SpringBoot整合Redis將對象寫入redis的實現
本文主要介紹了SpringBoot整合Redis將對象寫入redis的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-06-06

