<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Spring Boot 引起的“堆外內(nèi)存泄漏”排查及經(jīng)驗(yàn)總結(jié)

          共 4425字,需瀏覽 9分鐘

           ·

          2022-06-11 23:52

          點(diǎn)擊下方“IT牧場(chǎng)”,選擇“設(shè)為星標(biāo)”

          為了更好地實(shí)現(xiàn)對(duì)項(xiàng)目的管理,我們將組內(nèi)一個(gè)項(xiàng)目遷移到 MDP 框架(基于 SpringBoot),隨后我們就發(fā)現(xiàn)系統(tǒng)會(huì)頻繁報(bào)出 Swap 區(qū)域使用量過(guò)高的異常。


          筆者被叫去幫忙查看原因,發(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)存如下圖所示:

          top 命令顯示的內(nèi)存情況


          排查過(guò)程


          | 使用 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)存分布如下:

          jcmd 顯示的內(nèi)存情況


          發(fā)現(xiàn)命令顯示的 committed 的內(nèi)存小于物理內(nèi)存,因?yàn)?jcmd 命令顯示的內(nèi)存包含堆內(nèi)內(nèi)存、Code 區(qū)域、通過(guò) unsafe.allocateMemory 和 DirectByteBuffer 申請(qǐng)的內(nèi)存,但是不包含其他 Native Code(C 代碼)申請(qǐng)的堆外內(nèi)存。


          所以猜測(cè)是使用 Native Code 申請(qǐng)內(nèi)存所導(dǎo)致的問(wèn)題。


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

          pmap 顯示的內(nèi)存情況


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

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


          首先,使用了 gperftools 去定位問(wèn)題,gperftools 的使用方法可以參考 gperftools:

          https://github.com/gperftools/gperftools


          gperftools 的監(jiān)控如下:

          gperftools 監(jiān)控


          從上圖可以看出:使用 malloc 申請(qǐng)的的內(nèi)存最高到 3G 之后就釋放了,之后始終維持在 700M-800M。


          筆者第一反應(yīng)是:難道 Native Code 中沒(méi)有使用 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)槭褂?gperftools 沒(méi)有追蹤到這些內(nèi)存,于是直接使用命令“strace -f -e”brk,mmap,munmap” -p pid”追蹤向 OS 申請(qǐng)內(nèi)存請(qǐng)求,但是并沒(méi)有發(fā)現(xiàn)有可疑內(nèi)存申請(qǐng)。


          strace 監(jiān)控如下圖所示:

          strace 監(jiān)控


          接著,使用 GDB 去 dump 可疑內(nèi)存,為使用 strace 沒(méi)有追蹤到可疑內(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)容,如下:

          gperftools 監(jiān)控


          從內(nèi)容上來(lá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)存空間。


          截圖如下:

          strace 監(jiān)控


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

          strace 申請(qǐng)內(nèi)容對(duì)應(yīng)的 pmap 地址空間


          最后,使用 jstack 去查看對(duì)應(yīng)的線程,因?yàn)?strace 命令中已經(jīng)顯示申請(qǐng)內(nèi)存的線程 ID。


          直接使用命令 jstack pid 去查看線程棧,找到對(duì)應(yīng)的線程棧(注意 10 進(jìn)制和 16 進(jìn)制轉(zhuǎn)換)如下:

          strace 申請(qǐng)空間的線程棧


          這里基本上就可以看出問(wèn)題來(lái)了:MCC(美團(tuán)統(tǒng)一配置中心)使用了 Reflections 進(jìn)行掃包,底層使用了 SpringBoot 去加載 JAR。


          因?yàn)榻鈮?JAR 使用 Inflater 類,需要用到堆外內(nèi)存,然后使用 Btrace 去追蹤這個(gè)類,棧如下:

          btrace 追蹤棧


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


          | 為什么堆外內(nèi)存沒(méi)有釋放掉呢?

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

          • 為什么使用舊的框架沒(méi)有問(wèn)題?

          • 為什么堆外內(nèi)存沒(méi)有釋放?

          • 為什么內(nèi)存大小都是 64M,JAR 大小不可能這么大,而且都是一樣大?

          • 為什么 gperftools 最終顯示使用的的內(nèi)存大小是 700M 左右,解壓包真的沒(méi)有使用 malloc 申請(qǐng)內(nèi)存嗎?


          帶著疑問(wèn),筆者直接看了一下 SpringBoot Loader 那一塊的源碼。發(fā)現(xiàn) SpringBoot 對(duì) Java JDK 的 InflaterInputStream 進(jìn)行了包裝并且使用了 Inflater,而 Inflater 本身用于解壓 JAR 包的需要用到堆外內(nèi)存。


          而包裝之后的類 ZipInflaterInputStream 沒(méi)有釋放 Inflater 持有的堆外內(nèi)存。于是筆者以為找到了原因,立馬向 SpringBoot 社區(qū)反饋了這個(gè) Bug。


          但是反饋之后,筆者就發(fā)現(xiàn) Inflater 這個(gè)對(duì)象本身實(shí)現(xiàn)了 finalize 方法,在這個(gè)方法中有調(diào)用釋放堆外內(nèi)存的邏輯。也就是說(shuō) SpringBoot 依賴于 GC 釋放堆外內(nèi)存。


          筆者使用 jmap 查看堆內(nèi)對(duì)象時(shí),發(fā)現(xiàn)已經(jīng)基本上沒(méi)有 Inflater 這個(gè)對(duì)象了。于是就懷疑 GC 的時(shí)候,沒(méi)有調(diào)用 finalize。


          帶著這樣的懷疑,筆者把 Inflater 進(jìn)行包裝在 SpringBoot Loader 里面替換成自己包裝的 Inflater,在 finalize 進(jìn)行打點(diǎn)監(jiān)控,結(jié)果 finalize 方法確實(shí)被調(diào)用了。


          于是筆者又去看了 Inflater 對(duì)應(yīng)的 C 代碼,發(fā)現(xiàn)初始化的使用了 malloc 申請(qǐng)內(nèi)存,end 的時(shí)候也調(diào)用了 free 去釋放內(nèi)存。


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


          這時(shí),再返過(guò)來(lái)看 gperftools 的內(nèi)存分布情況,發(fā)現(xiàn)使用 SpringBoot 時(shí),內(nèi)存使用一直在增加,突然某個(gè)點(diǎn)內(nèi)存使用下降了好多(使用量直接由 3G 降為 700M 左右)。


          這個(gè)點(diǎn)應(yīng)該就是 GC 引起的,內(nèi)存應(yīng)該釋放了,但是在操作系統(tǒng)層面并沒(méi)有看到內(nèi)存變化,那是不是沒(mé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)存地址分布如下:

          gperftools 顯示的內(nèi)存地址分布


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


          原文如下:

          glib 內(nèi)存池說(shuō)明


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


          為了驗(yàn)證是內(nèi)存池搞的鬼,筆者就簡(jiǎn)單寫(xiě)個(gè)不帶內(nèi)存池的內(nèi)存分配器。


          使用命令 gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so 生成動(dòng)態(tài)庫(kù),然后使用 export LD_PRELOAD=zjbmalloc.so 替換掉 glibc 的內(nèi)存分配器。


          其中代碼 Demo 如下:
          #include
          #include
          #include
          #include
          //作者使用的64位機(jī)器,sizeof(size_t)也就是sizeof(long)?
          void*?malloc?(?size_t?size?)
          {
          ???long*?ptr?=?mmap(?0,?size?+?sizeof(long),?PROT_READ?|?PROT_WRITE,?MAP_PRIVATE?|?MAP_ANONYMOUS,?0,?0?);
          ???if?(ptr?==?MAP_FAILED)?{
          ??????return?NULL;
          ???}
          ???*ptr?=?size;?????????????????????//?First?8?bytes?contain?length.
          ???return?(void*)(&ptr[1]);????????//?Memory?that?is?after?length?variable
          }

          void?*calloc(size_t?n,?size_t?size)?{
          ?void*?ptr?=?malloc(n?*?size);
          ?if?(ptr?==?NULL)?{
          ????return?NULL;
          ?}
          ?memset(ptr,?0,?n?*?size);
          ?return?ptr;
          }
          void?*realloc(void?*ptr,?size_t?size)
          {
          ?if?(size?==?0)?{
          ????free(ptr);
          ????return?NULL;
          ?}
          ?if?(ptr?==?NULL)?{
          ????return?malloc(size);
          ?}
          ?long?*plen?=?(long*)ptr;
          ?plen--;??????????????????????????//?Reach?top?of?memory
          ?long?len?=?*plen;
          ?if?(size?<=?len)?{
          ????return?ptr;
          ?}
          ?void*?rptr?=?malloc(size);
          ?if?(rptr?==?NULL)?{
          ????free(ptr);
          ????return?NULL;
          ?}
          ?rptr?=?memcpy(rptr,?ptr,?len);
          ?free(ptr);
          ?return?rptr;
          }

          void?free?(void*?ptr?)
          {
          ???if?(ptr?==?NULL)?{
          ?????return;
          ???}
          ???long?*plen?=?(long*)ptr;
          ???plen--;??????????????????????????//?Reach?top?of?memory
          ???long?len?=?*plen;???????????????//?Read?length
          ???munmap((void*)plen,?len?+?sizeof(long));
          }


          通過(guò)在自定義分配器當(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)角度來(lái)看進(jìn)程占用的內(nèi)存差別很大(這里只是監(jiān)控堆外內(nèi)存)。


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

          內(nèi)存測(cè)試對(duì)比


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


          因?yàn)樽远x內(nèi)存分配器采用的是 mmap 分配內(nèi)存,mmap 分配內(nèi)存按需向上取整到整數(shù)個(gè)頁(yè),所以存在著巨大的空間浪費(fèi)。


          通過(guò)監(jiān)控發(fā)現(xiàn)最終申請(qǐng)的頁(yè)面數(shù)目在 536k 個(gè)左右,那實(shí)際上向系統(tǒng)申請(qǐng)的內(nèi)存等于 512k * 4k(pagesize) = 2G。


          為什么這個(gè)數(shù)據(jù)大于 1.7G 呢?因?yàn)椴僮飨到y(tǒng)采取的是延遲分配的方式,通過(guò) mmap 向系統(tǒng)申請(qǐng)內(nèi)存的時(shí)候,系統(tǒng)僅僅返回內(nèi)存地址并沒(méi)有分配真實(shí)的物理內(nèi)存。


          只有在真正使用的時(shí)候,系統(tǒng)產(chǎn)生一個(gè)缺頁(yè)中斷,然后再分配實(shí)際的物理 Page。


          總結(jié)


          流程圖


          整個(gè)內(nèi)存分配的流程如上圖所示。MCC 掃包的默認(rèn)配置是掃描所有的 JAR 包。在掃描包的時(shí)候,SpringBoot 不會(huì)主動(dòng)去釋放堆外內(nèi)存,導(dǎo)致在掃描階段,堆外內(nèi)存占用量一直持續(xù)飆升。


          當(dāng)發(fā)生 GC 的時(shí)候,SpringBoot 依賴于 finalize 機(jī)制去釋放了堆外內(nèi)存;但是 glibc 為了性能考慮,并沒(méi)有真正把內(nèi)存歸返到操作系統(tǒng),而是留下來(lái)放入內(nèi)存池了,導(dǎo)致應(yīng)用層以為發(fā)生了“內(nèi)存泄漏”。所以修改?MCC?的配置路徑為特定的?JAR?包,問(wèn)題解決。


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

          轉(zhuǎn)自:紀(jì)兵

          鏈接:tech.meituan.com/2019/01/03/spring-boot-native-memory-leak.html

          干貨分享

          最近將個(gè)人學(xué)習(xí)筆記整理成冊(cè),使用PDF分享。關(guān)注我,回復(fù)如下代碼,即可獲得百度盤地址,無(wú)套路領(lǐng)取!

          ?001:《Java并發(fā)與高并發(fā)解決方案》學(xué)習(xí)筆記;?002:《深入JVM內(nèi)核——原理、診斷與優(yōu)化》學(xué)習(xí)筆記;?003:《Java面試寶典》?004:《Docker開(kāi)源書(shū)》?005:《Kubernetes開(kāi)源書(shū)》?006:《DDD速成(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)速成)》?007:全部?008:加技術(shù)群討論

          加個(gè)關(guān)注不迷路

          喜歡就點(diǎn)個(gè)"在看"唄^_^

          瀏覽 52
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色逼逼网站 | 九九精品久久久久久久久无码人妻 | 日本一区二区在线 | 激情五月天色青五月天 | 色黄网站在线观看 |