Java服務(wù)假死后續(xù)之內(nèi)存溢出的原因分析
一、現(xiàn)象分析
上篇博客說到,Java服務(wù)假死的原因是使用了Guava緩存,30分鐘的有效期導(dǎo)致Full GC無法回收內(nèi)存。經(jīng)過優(yōu)化后,已經(jīng)不再使用Guava緩存,實(shí)時(shí)查詢數(shù)據(jù)。從短期效果來看,確實(shí)解決了無法回收內(nèi)存的問題,但是服務(wù)運(yùn)行幾天后,發(fā)現(xiàn)內(nèi)存又逐漸被占滿,F(xiàn)ull GC后只能回收一小部分。
從上圖可以看出,一次Full GC后,老年代基本上沒有回收多少內(nèi)存,占比從99.86%降到99.70%。
二、原因排查
到底是什么對(duì)象占據(jù)這么大的內(nèi)存,并且無法被JVM垃圾回收呢。在上一篇博客中已經(jīng)移除了Guava緩存,按理說不應(yīng)該有無法回收的對(duì)象了。那么,很明顯這應(yīng)該是代碼問題導(dǎo)致了內(nèi)存泄露,現(xiàn)在需要知道哪些對(duì)象無法被回收,從而定位出代碼哪里有BUG。這里采用jmap -histo:live 201349|head -10命令打印出GC后存活的對(duì)象。
從上圖可以看出,還是之前存在Guava緩存里面的對(duì)象占據(jù)著大部分內(nèi)存,代碼修改為實(shí)時(shí)查詢后,每次用完數(shù)據(jù)都會(huì)從Map中剔除,按理不應(yīng)該有強(qiáng)引用去引用這些對(duì)象。光看代碼無法排查出哪里導(dǎo)致了內(nèi)存泄露,只能將GC后的內(nèi)存文件導(dǎo)出來進(jìn)行分析。這里采用jmap -dump:format=b,file=/data/heap.hprof命令將內(nèi)存文件導(dǎo)出來,用JDK自帶的visualVM打開。
這里拿ECBug對(duì)象進(jìn)行分析,從引用關(guān)系可以看出,ECBug對(duì)象被DataSetCenter引用,DataSetCenter就是實(shí)時(shí)查詢數(shù)據(jù)進(jìn)行存儲(chǔ)的一個(gè)ConcurrentHashMap,但每次用完數(shù)據(jù)后都會(huì)進(jìn)行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))后,都會(huì)在finally里面調(diào)用dataSetCenter.remove(accessCacheDataSetKey)把key刪除掉,這樣在GC時(shí)會(huì)自動(dòng)回收Value值。但是忽略了一個(gè)方法getModelDataInternal,該方法可能會(huì)遞歸調(diào)用realTimeQueryBusinessModelData方法,如果存在遞歸調(diào)用的話,那么由于可重入鎖lock還沒有完成解鎖,所以無法進(jìn)入if(!lock.isLocked())條件語句中進(jìn)行刪除key的操作,這樣就造成了一部分?jǐn)?shù)據(jù)無法被刪除,隨著時(shí)間的推移,內(nèi)存中的數(shù)據(jù)會(huì)越來越多。
三、故障解決
基于上述的代碼分析,改造如下所示。
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; }
這里當(dāng)modelQueryLock可重入鎖完全解鎖后,調(diào)用removeDataSetKeys方法,該方法會(huì)將dataSetCenter里面的key全部刪除,這樣在GC時(shí)就會(huì)回收不用的數(shù)據(jù)對(duì)象。這里采用兩個(gè)可重入鎖的目的是,如果只用一個(gè)modelQueryLock可重入鎖,那么當(dāng)modelQueryLock完全解鎖后,正在執(zhí)行removeDataSetKeys方法時(shí),其他線程就可以進(jìn)入該方法區(qū),發(fā)現(xiàn)dataSetCenter里面還沒有刪除完全,從而獲取里面的數(shù)據(jù),即if (!dataSetCenter.containsKey(accessCacheDataSetKey))為false,從而通過List<BusinessBean> cachedBeans = dataSetCenter.get(accessCacheDataSetKey).getData()直接獲取dataSetCenter里面的數(shù)據(jù),但是下一刻dataSetCenter里面可能已經(jīng)為空。因此,采用兩個(gè)可重入鎖,防止出現(xiàn)異常。
到此這篇關(guān)于Java服務(wù)假死后續(xù)之內(nèi)存溢出的文章就介紹到這了,更多相關(guān)Java 內(nèi)存溢出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springmvc異常映射2種實(shí)現(xiàn)方法
這篇文章主要介紹了Springmvc異常映射2種實(shí)現(xiàn)方法以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。,需要的朋友可以參考下2020-05-05SpringBoot使用異步線程池實(shí)現(xiàn)生產(chǎn)環(huán)境批量數(shù)據(jù)推送
本文主要介紹了SpringBoot使用異步線程池實(shí)現(xiàn)生產(chǎn)環(huán)境批量數(shù)據(jù)推送,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-02-02Springboot處理CORS跨域請(qǐng)求的三種方法
這篇文章主要介紹了Springboot處理CORS跨域請(qǐng)求的三種方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06SpringBoot2.0整合Shiro框架實(shí)現(xiàn)用戶權(quán)限管理的示例
這篇文章主要介紹了SpringBoot2.0整合Shiro框架實(shí)現(xiàn)用戶權(quán)限管理的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08SpringBoot異步任務(wù)實(shí)現(xiàn)下單校驗(yàn)庫存的項(xiàng)目實(shí)踐
在開發(fā)中,異步任務(wù)應(yīng)用的場景非常的廣泛,本文主要介紹了SpringBoot異步任務(wù)實(shí)現(xiàn)下單校驗(yàn)庫存的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09