前天同事在我們的數(shù)據(jù)服務(wù)spring boot項(xiàng)目中發(fā)現(xiàn)請(qǐng)求某個(gè)接口昌简,導(dǎo)致內(nèi)存不停增長(zhǎng),最后導(dǎo)致內(nèi)存溢出绒怨,程序崩潰的問題纯赎。最初懷疑是不是有什么大對(duì)象在JVM中進(jìn)行了垃圾回收也沒有釋放,順著這個(gè)思路一步步排查南蹂。
- 使用jstat查看垃圾回收頻率
jstat -gc 1 250 20
發(fā)現(xiàn)MinorGC頻率很低犬金,F(xiàn)ullGC頻率很高。初步定位是老年代內(nèi)存分配太少引起的碎紊。過(guò)然查看java程序啟動(dòng)參數(shù)發(fā)現(xiàn)以下設(shè)定:"-Xms300m","-Xmx300m","-MaxNewSize=300"
很顯然這個(gè)配置存在明顯的問題佑附,整個(gè)堆分配300m內(nèi)存,給新生代的是300m仗考,導(dǎo)致老年代幾乎沒有內(nèi)存可以使用音同,所以會(huì)導(dǎo)致頻繁的FullGC。把MaxNewSize去掉秃嗜,使用系統(tǒng)默認(rèn)的2:8权均。然后觀察垃圾回收情況,這次不會(huì)頻繁發(fā)生FullGC了锅锨,但是內(nèi)存還是一直增長(zhǎng)叽赊,直到內(nèi)存溢出。
- 使用jmap查看堆對(duì)象
jmap -dump:format=b,file=test.bin pid
因?yàn)槌霈F(xiàn)的問題在生產(chǎn)服務(wù)器中必搞,使用JProfiler遠(yuǎn)程連接以前沒有試過(guò)必指。暫時(shí)先用JVM自帶的工具jmap把堆快照下載下來(lái),然后使用本地JProfiler進(jìn)行分析恕洲。初步定為發(fā)現(xiàn)LauchedURLClassLoader占用了很多內(nèi)存塔橡,但是由于分析的是快照,沒法知道該對(duì)象的增長(zhǎng)速度霜第,無(wú)果葛家。
-
在本地使用JProfiler實(shí)時(shí)監(jiān)控分析
使用JVM自帶的工具能看到的信息有限。權(quán)衡使用JProfiler遠(yuǎn)程連接定位還不如在本地開啟調(diào)試環(huán)境泌类,然后使用JProfiler附加該java進(jìn)程監(jiān)控癞谒。標(biāo)記內(nèi)存分配比較多的大對(duì)象,然后模擬點(diǎn)擊持續(xù)請(qǐng)求,發(fā)現(xiàn)很多對(duì)象的調(diào)用實(shí)例都一直在增加弹砚,然后觀察線程一直在增加并且停止請(qǐng)求双仍,線程也沒有釋放。線程名稱I/O dispatcher迅栅,查看線程堆棧是一個(gè)NIO線程殊校,由AbstractMultiworkerIOReactor創(chuàng)建。通過(guò)包名不難定位是引用的CloseableHttpAsyncClient異步http請(qǐng)求的問題读存。然后在代碼端查找使用該類的地方,發(fā)現(xiàn)該段代碼中使用ES進(jìn)行搜索呕屎,而搜索用的對(duì)象RestHighLevelClient让簿,內(nèi)部使用的是異步http請(qǐng)求。至此問題已經(jīng)定位到秀睛,是es使用的相關(guān)問題尔当。經(jīng)驗(yàn)證把es相關(guān)代碼注釋掉,程序運(yùn)行正常蹂安,線程分配也正常椭迎。
image.png - 調(diào)整RestHighLevelClient的使用方式
定位到是RestHighLevelClient使用的問題,開始懷疑是否是使用的姿勢(shì)不對(duì)田盈,于是查看文檔畜号,然后設(shè)置CloseableHttpAsyncClient的IO線程數(shù)等一系列參數(shù),線程還是暴漲允瞧,無(wú)果简软。下面是在es github和論壇中查看的一些issues.
// Hight I/O dispatch Thread
https://github.com/elastic/elasticsearch/issues/61675
// Direct buffer memory problems with RestHighLevelClient
https://discuss.elastic.co/t/direct-buffer-memory-problems-with-resthighlevelclient/106647)
- 通過(guò)源碼定位到問題
無(wú)奈只能硬著頭皮查看源碼(具體對(duì)CloseableHttpAsyncClient源碼分析有時(shí)間再寫一篇筆記),無(wú)意跟蹤代碼的過(guò)程中發(fā)現(xiàn)RestHighLevelClient每次請(qǐng)求的過(guò)程中述暂,都會(huì)初始化痹升,然后初始化資源也沒有釋放。
public RestHighLevelClient(RestClientBuilder restClientBuilder) {
this(restClientBuilder, Collections.emptyList());
}
繼續(xù)跟蹤本地自己寫的代碼畦韭,發(fā)現(xiàn)在一個(gè)Optional初始化的過(guò)程中在orElse有一個(gè)初始化的代碼疼蛾。經(jīng)調(diào)試驗(yàn)證,原來(lái)orElse就算不走該分支艺配,程序執(zhí)行的過(guò)程中會(huì)預(yù)先初始化察郁。在RestHighLevelClient預(yù)先分配了線程,然后程序也沒有釋放妒挎,所以導(dǎo)致線程溢出和內(nèi)存溢出绳锅,去掉orElse,然后重新測(cè)試觀察JProfiler中線程的趨勢(shì)圖已經(jīng)正常酝掩,至此問題解決鳞芙。