最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

關(guān)于Spring?Boot內(nèi)存泄露排查的記錄

 更新時(shí)間:2022年06月17日 09:01:27   作者:weixin_42073629  
這篇文章主要介紹了關(guān)于Spring?Boot內(nèi)存泄露排查的記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

在項(xiàng)目遷移到Spring Boot之后,發(fā)生內(nèi)存使用量過高的問題。本文介紹了整個(gè)排查過程以及使用到的工具,也非常適用于其他堆外內(nèi)存排查。

背景

為了更好地實(shí)現(xiàn)對(duì)項(xiàng)目的管理,我們將組內(nèi)一個(gè)項(xiàng)目遷移到MDP框架(基于Spring Boot),隨后我們就發(fā)現(xiàn)系統(tǒng)會(huì)頻繁報(bào)出Swap區(qū)域使用量過高的異常。筆者被叫去幫忙查看原因,發(fā)現(xiàn)配置了4G堆內(nèi)內(nèi)存,但是實(shí)際使用的物理內(nèi)存竟然高達(dá)7G,確實(shí)不正常。

JVM參數(shù)配置是:

-XX:MetaspaceSize=256M 
-XX:MaxMetaspaceSize=256M 
-XX:+AlwaysPreTouch 
-XX:ReservedCodeCacheSize=128m 
-XX:InitialCodeCacheSize=128m, 
-Xss512k -Xmx4g -Xms4g,-XX:+UseG1GC 
-XX:G1HeapRegionSize=4M

實(shí)際使用的物理內(nèi)存如下圖所示:

圖片

排查過程

1.使用Java層面的工具定位內(nèi)存區(qū)域

(堆內(nèi)內(nèi)存、Code區(qū)域或者使用unsafe.allocateMemory和DirectByteBuffer申請(qǐng)的堆外內(nèi)存)

筆者在項(xiàng)目中添加-XX:NativeMemoryTracking=detailJVM參數(shù)重啟項(xiàng)目,使用命令jcmd pid VM.native_memory detail查看到的內(nèi)存分布如下: 

圖片

發(fā)現(xiàn)命令顯示的committed的內(nèi)存小于物理內(nèi)存,因?yàn)閖cmd命令顯示的內(nèi)存包含堆內(nèi)內(nèi)存、Code區(qū)域、通過unsafe.allocateMemory和DirectByteBuffer申請(qǐng)的內(nèi)存,但是不包含其他Native Code(C代碼)申請(qǐng)的堆外內(nèi)存。所以猜測是使用Native Code申請(qǐng)內(nèi)存所導(dǎo)致的問題。

為了防止誤判,筆者使用了pmap查看內(nèi)存分布,發(fā)現(xiàn)大量64M地址;而這些地址空間不在jcmd命令所給出的地址空間里面,基本上可斷定就是這些64M的內(nèi)存所導(dǎo)致。

圖片

2. 使用系統(tǒng)層面的工具定位堆外內(nèi)存

因?yàn)楣P者已經(jīng)基本上確定是Native Code所引起,而Java層面的工具不便于排查此類問題,只能使用系統(tǒng)層面的工具去定位問題。

首先,使用了gperftools去定位問題

gperftools的使用方法可以參考gperftools,gperftools的監(jiān)控如下:

圖片

從上圖可以看出:使用malloc申請(qǐng)的內(nèi)存最高到3G之后就釋放了,之后始終維持在700M-800M。筆者第一反應(yīng)是:難道Native Code中沒有使用malloc申請(qǐng),直接使用mmap/brk申請(qǐng)的?(gperftools原理就使用動(dòng)態(tài)鏈接的方式替換了操作系統(tǒng)默認(rèn)的內(nèi)存分配器(glibc)。)

然后,使用strace去追蹤系統(tǒng)調(diào)用

因?yàn)槭褂胓perftools沒有追蹤到這些內(nèi)存,于是直接使用命令strace -f -e"brk,mmap,munmap" -p pid追蹤向OS申請(qǐng)內(nèi)存請(qǐng)求,但是并沒有發(fā)現(xiàn)有可疑內(nèi)存申請(qǐng)。strace監(jiān)控如下圖所示:

圖片

接著,使用GDB去dump可疑內(nèi)存

因?yàn)槭褂胹trace沒有追蹤到可疑內(nèi)存申請(qǐng);于是想著看看內(nèi)存中的情況。

就是直接使用命令gdp -pid pid進(jìn)入GDB之后,然后使用命令dump memory mem.bin startAddress endAddressdump內(nèi)存,其中startAddress和endAddress可以從/proc/pid/smaps中查找。

然后使用strings mem.bin查看dump的內(nèi)容,如下:

圖片

從內(nèi)容上來看,像是解壓后的JAR包信息。讀取JAR包信息應(yīng)該是在項(xiàng)目啟動(dòng)的時(shí)候,那么在項(xiàng)目啟動(dòng)之后使用strace作用就不是很大了。所以應(yīng)該在項(xiàng)目啟動(dòng)的時(shí)候使用strace,而不是啟動(dòng)完成之后。

再次,項(xiàng)目啟動(dòng)時(shí)使用strace去追蹤系統(tǒng)調(diào)用

項(xiàng)目啟動(dòng)使用strace追蹤系統(tǒng)調(diào)用,發(fā)現(xiàn)確實(shí)申請(qǐng)了很多64M的內(nèi)存空間,截圖如下:

圖片

使用該mmap申請(qǐng)的地址空間在pmap對(duì)應(yīng)如下:

圖片

最后,使用jstack去查看對(duì)應(yīng)的線程

因?yàn)閟trace命令中已經(jīng)顯示申請(qǐng)內(nèi)存的線程ID。直接使用命令jstack pid去查看線程棧,找到對(duì)應(yīng)的線程棧(注意10進(jìn)制和16進(jìn)制轉(zhuǎn)換)如下:

圖片

這里基本上就可以看出問題來了:MCC(美團(tuán)統(tǒng)一配置中心)使用了Reflections進(jìn)行掃包,底層使用了Spring Boot去加載JAR。因?yàn)榻鈮篔AR使用Inflater類,需要用到堆外內(nèi)存,然后使用Btrace去追蹤這個(gè)類,棧如下:

圖片

然后查看使用MCC的地方,發(fā)現(xiàn)沒有配置掃包路徑,默認(rèn)是掃描所有的包。于是修改代碼,配置掃包路徑,發(fā)布上線后內(nèi)存問題解決。

3. 為什么堆外內(nèi)存沒有釋放掉呢?

雖然問題已經(jīng)解決了,但是有幾個(gè)疑問:

  • 為什么使用舊的框架沒有問題?
  • 為什么堆外內(nèi)存沒有釋放?
  • 為什么內(nèi)存大小都是64M,JAR大小不可能這么大,而且都是一樣大?
  • 為什么gperftools最終顯示使用的內(nèi)存大小是700M左右,解壓包真的沒有使用malloc申請(qǐng)內(nèi)存嗎?

帶著疑問,筆者直接看了一下Spring Boot Loader那一塊的源碼。發(fā)現(xiàn)Spring Boot對(duì)Java JDK的InflaterInputStream進(jìn)行了包裝并且使用了Inflater,而Inflater本身用于解壓JAR包的需要用到堆外內(nèi)存。而包裝之后的類ZipInflaterInputStream沒有釋放Inflater持有的堆外內(nèi)存。于是筆者以為找到了原因,立馬向Spring Boot社區(qū)反饋了這個(gè)Bug。但是反饋之后,筆者就發(fā)現(xiàn)Inflater這個(gè)對(duì)象本身實(shí)現(xiàn)了finalize方法,在這個(gè)方法中有調(diào)用釋放堆外內(nèi)存的邏輯。也就是說Spring Boot依賴于GC釋放堆外內(nèi)存。

筆者使用jmap查看堆內(nèi)對(duì)象時(shí),發(fā)現(xiàn)已經(jīng)基本上沒有Inflater這個(gè)對(duì)象了。于是就懷疑GC的時(shí)候,沒有調(diào)用finalize。帶著這樣的懷疑,筆者把Inflater進(jìn)行包裝在Spring Boot Loader里面替換成自己包裝的Inflater,在finalize進(jìn)行打點(diǎn)監(jiān)控,結(jié)果finalize方法確實(shí)被調(diào)用了。于是筆者又去看了Inflater對(duì)應(yīng)的C代碼,發(fā)現(xiàn)初始化的時(shí)候使用了malloc申請(qǐng)內(nèi)存,end的時(shí)候也調(diào)用了free去釋放內(nèi)存。

此刻,筆者只能懷疑free的時(shí)候沒有真正釋放內(nèi)存,便把Spring Boot包裝的InflaterInputStream替換成Java JDK自帶的,發(fā)現(xiàn)替換之后,內(nèi)存問題也得以解決了。

這時(shí),再返過來看gperftools的內(nèi)存分布情況,發(fā)現(xiàn)使用Spring Boot時(shí),內(nèi)存使用一直在增加,突然某個(gè)點(diǎn)內(nèi)存使用下降了好多(使用量直接由3G降為700M左右)。這個(gè)點(diǎn)應(yīng)該就是GC引起的,內(nèi)存應(yīng)該釋放了,但是在操作系統(tǒng)層面并沒有看到內(nèi)存變化,那是不是沒有釋放到操作系統(tǒng),被內(nèi)存分配器持有了呢?

繼續(xù)探究,發(fā)現(xiàn)系統(tǒng)默認(rèn)的內(nèi)存分配器(glibc 2.12版本)和使用gperftools內(nèi)存地址分布差別很明顯,2.5G地址使用smaps發(fā)現(xiàn)它是屬于Native Stack。內(nèi)存地址分布如下:

圖片

到此,基本上可以確定是內(nèi)存分配器在搗鬼;搜索了一下glibc 64M,發(fā)現(xiàn)glibc從2.11開始對(duì)每個(gè)線程引入內(nèi)存池(64位機(jī)器大小就是64M內(nèi)存),原文如下:

圖片

按照文中所說去修改MALLOC_ARENA_MAX環(huán)境變量,發(fā)現(xiàn)沒什么效果。查看tcmalloc(gperftools使用的內(nèi)存分配器)也使用了內(nèi)存池方式。

為了驗(yàn)證是內(nèi)存池搞的鬼,筆者就簡單寫個(gè)不帶內(nèi)存池的內(nèi)存分配器。使用命令gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so生成動(dòng)態(tài)庫,然后使用export LD_PRELOAD=zjbmalloc.so替換掉glibc的內(nèi)存分配器。其中代碼Demo如下:

圖片

通過在自定義分配器當(dāng)中埋點(diǎn)可以發(fā)現(xiàn)其實(shí)程序啟動(dòng)之后應(yīng)用實(shí)際申請(qǐng)的堆外內(nèi)存始終在700M-800M之間,gperftools監(jiān)控顯示內(nèi)存使用量也是在700M-800M左右。但是從操作系統(tǒng)角度來看進(jìn)程占用的內(nèi)存差別很大(這里只是監(jiān)控堆外內(nèi)存)。

筆者做了一下測試,使用不同分配器進(jìn)行不同程度的掃包,占用的內(nèi)存如下:

圖片

為什么自定義的malloc申請(qǐng)800M,最終占用的物理內(nèi)存在1.7G呢?

因?yàn)樽远x內(nèi)存分配器采用的是mmap分配內(nèi)存,mmap分配內(nèi)存需要按需向上取整到整數(shù)個(gè)頁,所以存在著巨大的空間浪費(fèi)。通過監(jiān)控發(fā)現(xiàn)最終申請(qǐng)的頁面數(shù)目在536k個(gè)左右,那實(shí)際上向系統(tǒng)申請(qǐng)的內(nèi)存等于512k * 4k(pagesize) = 2G。 為什么這個(gè)數(shù)據(jù)大于1.7G呢?

因?yàn)椴僮飨到y(tǒng)采取的是延遲分配的方式,通過mmap向系統(tǒng)申請(qǐng)內(nèi)存的時(shí)候,系統(tǒng)僅僅返回內(nèi)存地址并沒有分配真實(shí)的物理內(nèi)存。只有在真正使用的時(shí)候,系統(tǒng)產(chǎn)生一個(gè)缺頁中斷然后再分配實(shí)際的物理Page。

總結(jié)

圖片

整個(gè)內(nèi)存分配的流程如上圖所示。MCC掃包的默認(rèn)配置是掃描所有的JAR包。在掃描包的時(shí)候,Spring Boot不會(huì)主動(dòng)去釋放堆外內(nèi)存,導(dǎo)致在掃描階段,堆外內(nèi)存占用量一直持續(xù)飆升。當(dāng)發(fā)生GC的時(shí)候,Spring Boot依賴于finalize機(jī)制去釋放了堆外內(nèi)存;但是glibc為了性能考慮,并沒有真正把內(nèi)存歸返到操作系統(tǒng),而是留下來放入內(nèi)存池了,導(dǎo)致應(yīng)用層以為發(fā)生了“內(nèi)存泄漏”。所以修改MCC的配置路徑為特定的JAR包,問題解決。

筆者在發(fā)表這篇文章時(shí),發(fā)現(xiàn)Spring Boot的最新版本(2.0.5.RELEASE)已經(jīng)做了修改,在ZipInflaterInputStream主動(dòng)釋放了堆外內(nèi)存不再依賴GC;所以Spring Boot升級(jí)到最新版本,這個(gè)問題也可以得到解決。

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java中遍歷Map集合的5種方式總結(jié)

    Java中遍歷Map集合的5種方式總結(jié)

    這篇文章主要給大家介紹了關(guān)于Java中遍歷Map集合的5種方式,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-01-01
  • Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

    Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

    本文詳細(xì)解讀一下volatile關(guān)鍵字如何保證變量在多線程之間的可見性,對(duì)Java中volatile關(guān)鍵字實(shí)現(xiàn)原理感興趣的朋友一起通過本文學(xué)習(xí)吧
    2017-06-06
  • 基于Java實(shí)現(xiàn)圖片相似度對(duì)比的示例代碼

    基于Java實(shí)現(xiàn)圖片相似度對(duì)比的示例代碼

    很多時(shí)候我們需要將兩個(gè)圖片進(jìn)行對(duì)比,確定兩個(gè)圖片的相似度。本文將利用Java和OpenCV庫實(shí)現(xiàn)圖片相似度對(duì)比,感興趣的可以動(dòng)手嘗試一下
    2022-07-07
  • Java版本的回文字算法(java版本)

    Java版本的回文字算法(java版本)

    本文給大家分享一段java代碼關(guān)于回文字算法的實(shí)例代碼,代碼簡單易懂,需要的朋友一起看看吧
    2016-10-10
  • java的接口解耦方式

    java的接口解耦方式

    這篇文章主要介紹了java的接口解耦方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • java多線程編程之Synchronized塊同步方法

    java多線程編程之Synchronized塊同步方法

    這篇文章主要介紹了java多線程編程之Synchronized塊同步方法,synchronized關(guān)鍵字又稱同步鎖,當(dāng)方法執(zhí)行完后,會(huì)自動(dòng)釋放鎖鎖,只有一個(gè)線程能進(jìn)入此方法,看看下文中各種例子對(duì)synchronized的詳細(xì)解釋
    2015-12-12
  • 使用Java代碼獲取服務(wù)器性能信息及局域網(wǎng)內(nèi)主機(jī)名

    使用Java代碼獲取服務(wù)器性能信息及局域網(wǎng)內(nèi)主機(jī)名

    這篇文章主要介紹了使用Java代碼獲取服務(wù)器性能信息及局域網(wǎng)內(nèi)主機(jī)名的方法,方便對(duì)服務(wù)器的遠(yuǎn)程管理和團(tuán)隊(duì)協(xié)作時(shí)用到,而且文中的方法無需調(diào)用jni,需要的朋友可以參考下
    2015-11-11
  • 帶你了解Java中Static關(guān)鍵字的用法

    帶你了解Java中Static關(guān)鍵字的用法

    這篇文章主要介紹了JAVA Static關(guān)鍵字的用法,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下,希望能給你帶來幫助
    2021-08-08
  • Struts2實(shí)現(xiàn)文件上傳功能

    Struts2實(shí)現(xiàn)文件上傳功能

    這篇文章主要為大家詳細(xì)介紹了Struts2實(shí)現(xiàn)文件上傳功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • SpringMvc3+extjs4實(shí)現(xiàn)上傳與下載功能

    SpringMvc3+extjs4實(shí)現(xiàn)上傳與下載功能

    這篇文章主要為大家詳細(xì)介紹了SpringMvc3+extjs4實(shí)現(xiàn)上傳與下載功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-06-06

最新評(píng)論

娄烦县| 广丰县| 凌源市| 漠河县| 资溪县| 武鸣县| 枣庄市| 荣成市| 贵州省| 五大连池市| 中方县| 鲁甸县| 沁阳市| 固安县| 城口县| 始兴县| 彩票| 渝北区| 高唐县| 承德县| 南漳县| 潢川县| 阳原县| 嘉定区| 综艺| 梓潼县| 隆化县| 颍上县| 全椒县| 丹东市| 西丰县| 运城市| 高青县| 托里县| 阳原县| 宁德市| 永胜县| 仁寿县| 津南区| 洛南县| 泗水县|