Glibc 內(nèi)存分配與釋放機(jī)制詳解
一、引言
內(nèi)存對(duì)象的分配與釋放一直是后端開(kāi)發(fā)人員代碼設(shè)計(jì)中需要考慮的問(wèn)題,考慮不周極易造成內(nèi)存泄漏、內(nèi)存訪問(wèn)越界等問(wèn)題。在發(fā)生內(nèi)存異常后,開(kāi)發(fā)人員往往花費(fèi)大量時(shí)間排查用戶(hù)管理層代碼,而忽視了C運(yùn)行時(shí),庫(kù)層和操作系統(tǒng)層本身的實(shí)現(xiàn)也可能會(huì)帶來(lái)內(nèi)存問(wèn)題。本文先以一次線上內(nèi)存事故引出問(wèn)題,再逐步介紹 glibc 庫(kù)的內(nèi)存布局設(shè)計(jì)、內(nèi)存分配、釋放邏輯,最后給出相應(yīng)的解決方案。
二、內(nèi)存告警事件
在一次線上運(yùn)維過(guò)程中發(fā)現(xiàn)服務(wù)出現(xiàn)內(nèi)存告警。
【監(jiān)控系統(tǒng)-自定義監(jiān)控-告警-持續(xù)告警】
檢測(cè)規(guī)則: xxx內(nèi)存使用率監(jiān)測(cè):一般異常(>4096)
集群id:xxx
集群名稱(chēng): xxxxxx
異常對(duì)象(當(dāng)前值): xx.xx.xx.xx-xxxxxxx(11335)
開(kāi)始時(shí)間: 2023-08-10 17:10:30
告警時(shí)間: 2023-08-10 18:20:32
持續(xù)時(shí)間: 1h10m2s
異常比例: 2.1918 (8/365)
異常級(jí)別: 一般
備注:-
隨即查看服務(wù)相關(guān)監(jiān)控,判斷是業(yè)務(wù)流量激增帶來(lái)的內(nèi)存短時(shí)間增高,或是發(fā)生了內(nèi)存泄漏。
通過(guò)查看 OPS 和服務(wù)自身統(tǒng)計(jì)的內(nèi)存監(jiān)控,發(fā)現(xiàn)在告警時(shí)間內(nèi)存在業(yè)務(wù)流量突增現(xiàn)象,但是內(nèi)存已經(jīng)下降到正常值了。然而告警持續(xù)到了18:20依然沒(méi)有恢復(fù),跟監(jiān)控表現(xiàn)不符,登錄機(jī)器后發(fā)現(xiàn)實(shí)例的內(nèi)存并沒(méi)有恢復(fù),隨即懷疑用戶(hù)層發(fā)生內(nèi)存泄漏。
經(jīng)過(guò)分析,由于內(nèi)存統(tǒng)計(jì)代碼每次調(diào)用 new、delete 之后才會(huì)對(duì)統(tǒng)計(jì)值進(jìn)行增減,而監(jiān)控中服務(wù)統(tǒng)計(jì)內(nèi)存已經(jīng)下降,說(shuō)明已經(jīng)正常調(diào)用 delete 進(jìn)行內(nèi)存釋放,而操作系統(tǒng)層面發(fā)現(xiàn)內(nèi)存依然居高不下,懷疑使用的c運(yùn)行庫(kù) glibc 存在內(nèi)存釋放問(wèn)題。
三、glibc 內(nèi)存管理機(jī)制
3.1 glibc 簡(jiǎn)介
glibc 全稱(chēng)為 GUN C Library,是一個(gè)開(kāi)源的標(biāo)準(zhǔn)C庫(kù),其對(duì)操作系統(tǒng)相關(guān)調(diào)用進(jìn)行了封裝,提供包括數(shù)學(xué)、字符串、文件 I/O、內(nèi)存管理、多線程等方面標(biāo)準(zhǔn)函數(shù)和系統(tǒng)調(diào)用接口供用戶(hù)使用。
3.2 內(nèi)存管理布局
以 Linux 內(nèi)核 v2.6.7 之后的32位模式下的虛擬內(nèi)存布局方式為例:
- Kernel Space(內(nèi)核空間)— 存儲(chǔ)內(nèi)核和驅(qū)動(dòng)程序的代碼和數(shù)據(jù);
- Stack(棧區(qū))— 存儲(chǔ)程序執(zhí)行期間的本地變量和函數(shù)的參數(shù),從高地址向低地址生長(zhǎng);
- Memory Mapping Segment(內(nèi)存映射區(qū))— 簡(jiǎn)稱(chēng)為 mmap,用來(lái)文件或其他對(duì)象映射進(jìn)內(nèi)存;
- Heap(堆區(qū))— 動(dòng)態(tài)內(nèi)存分配區(qū)域,通過(guò) malloc、new、free 和 delete 等函數(shù)管理;
- BSS segment(未初始化變量區(qū))— 存儲(chǔ)未被初始化的全局變量和靜態(tài)變量;
- DATA segment(數(shù)據(jù)區(qū))— 存儲(chǔ)在源代碼中有預(yù)定義值的全局變量和靜態(tài)變量;
- TEXT segment(代碼區(qū))— 存儲(chǔ)只讀的程序執(zhí)行代碼,即機(jī)器指令。
其中 Heap 和 Mmap 區(qū)域是可以提供給用戶(hù)程序使用的虛擬內(nèi)存空間。
Heap 操作
操作系統(tǒng)提供了 brk() 函數(shù),c運(yùn)行時(shí)庫(kù)提供了 sbrk() 函數(shù)從 Heap 中申請(qǐng)內(nèi)存,函數(shù)聲明如下:
int brk(void *addr);
void *sbrk(intptr_t increment);
- brk() 通過(guò)設(shè)置進(jìn)程堆的結(jié)束地址進(jìn)行內(nèi)存分配與釋放,即可以一次性的分配或釋放一整段連續(xù)的內(nèi)存空間。比較適合于一次性分配大塊內(nèi)存的情況,如果設(shè)置的結(jié)束地址過(guò)大或過(guò)小會(huì)造成內(nèi)存碎片或內(nèi)存浪費(fèi)的問(wèn)題。
- sbrk 函數(shù)通過(guò)傳入的 increment 參數(shù)決定增加或減少堆空間的大小,可以動(dòng)態(tài)的多次分配或釋放空間達(dá)到需要多少內(nèi)存就申請(qǐng)多少內(nèi)存的效果,有效避免了內(nèi)存碎片和浪費(fèi)問(wèn)題。
Mmap 操作
在 Linux 中提供了 mmap() 和 munmap() 函數(shù)操作虛擬內(nèi)存空間,函數(shù)聲明如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
其中 mmap 能夠?qū)⑽募蛘咂渌麑?duì)象映射進(jìn)內(nèi)存,munmap 能夠刪除特定地址區(qū)域的內(nèi)存映射。
3.3 內(nèi)存分配器
開(kāi)源社區(qū)公開(kāi)了很多現(xiàn)成的內(nèi)存分配器,包括 dlmalloc、ptmalloc、jemalloc、tcmalloc......,由于 glibc 用的是 ptmalloc 所以本文只對(duì)該內(nèi)存分配器進(jìn)行介紹。
3.3.1 Arena(分配區(qū))
堆管理結(jié)構(gòu)如下所示:
struct malloc_state {
mutex_t mutex; /* Serialize access. */
int flags; /* Flags (formerly in max_fast). */
#if THREAD_STATS
/* Statistics for locking. Only used if THREAD_STATS is defined. */
long stat_lock_direct, stat_lock_loop, stat_lock_wait;
#endif
mfastbinptr fastbins[NFASTBINS]; /* Fastbins */
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins[NBINS * 2];
unsigned int binmap[BINMAPSIZE]; /* Bitmap of bins */
struct malloc_state *next; /* Linked list */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
ptmalloc 對(duì)進(jìn)程內(nèi)存是通過(guò)一個(gè)個(gè)的分配區(qū)進(jìn)行管理的,而分配區(qū)分為主分配區(qū)(arena)和非主分配區(qū)(narena),兩者區(qū)別在于主分配區(qū)中可以使用 sbrk 和 mmap 向操作系統(tǒng)申請(qǐng)內(nèi)存,而非主分配區(qū)只能通過(guò) mmap 申請(qǐng)內(nèi)存。
對(duì)于一個(gè)進(jìn)程,只有一個(gè)主分配區(qū)和若干個(gè)非主分配區(qū),主分配區(qū)只能由第一個(gè)線程來(lái)創(chuàng)建持有,其和非主分配區(qū)由環(huán)形鏈表的形式相互連接,整個(gè)分配區(qū)中通過(guò)變量互斥鎖支持多線程訪問(wèn)。
當(dāng)一個(gè)線程調(diào)用 malloc 申請(qǐng)內(nèi)存時(shí),該線程先查看線程私有變量中是否已經(jīng)存在一個(gè)分配區(qū)。如果存在,則對(duì)該分配區(qū)加鎖,加鎖成功的話就用該分配區(qū)進(jìn)行內(nèi)存分配;失敗的話則搜索環(huán)形鏈表找一個(gè)未加鎖的分配區(qū)。如果所有分配區(qū)都已經(jīng)加鎖,那么 malloc 會(huì)開(kāi)辟一個(gè)新的分配區(qū)加入環(huán)形鏈表并加鎖,用它來(lái)分配內(nèi)存。釋放操作同樣需要獲得鎖才能進(jìn)行。
3.3.2 chunk
ptmalloc 通過(guò) malloc_chunk 來(lái)管理內(nèi)存,定義如下:
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
- prev_size:存儲(chǔ)前一個(gè) chunk 的大小。如果前一個(gè) chunk 沒(méi)有被使用,則 prev_size 的值表示前一個(gè) chunk 的大小,如果前一 chunk 已被使用,則 prev_size 的值沒(méi)有意義。
- size:表示當(dāng)前 chunk 的大小,包括所請(qǐng)求的有效數(shù)據(jù)大小,以及堆塊頭部和尾部的管理信息等附加信息的大小。
- fd 和 bk:表示 chunk 在空閑鏈表中的前一個(gè)和后一個(gè)堆塊的指針。如果該 chunk 被占用,則這兩個(gè)指針沒(méi)有意義。
- fd_nextsize 和 bk_nextsize:表示同一空閑鏈表上下一個(gè)堆塊的指針。fd_nextsize 指向下一個(gè)比當(dāng)前 chunk 大小大的第一個(gè)空閑 chunk , bk_nextszie 指向前一個(gè)比當(dāng)前 chunk 大小小的第一個(gè)空閑 chunk,增加這兩個(gè)字段可以加快遍歷空閑 chunk ,并查找滿(mǎn)足需要的空閑 chunk 。
使用該數(shù)據(jù)結(jié)構(gòu)能夠更快的在鏈表中查找到空閑 chunk 并分配。
3.3.3 空閑鏈表(bins)
在 ptmalloc 中,會(huì)將大小相似的 chunk 鏈接起來(lái),叫做空閑鏈表(bins),總共有128個(gè) bin 供 ptmalloc 使用。用戶(hù)調(diào)用 free 函數(shù)釋放內(nèi)存的時(shí)候,ptmalloc 并不會(huì)立即將其歸還操作系統(tǒng),而是將其放入 bins 中,這樣下次再調(diào)用 malloc 函數(shù)申請(qǐng)內(nèi)存的時(shí)候,就會(huì)從 bins 中取出一塊返回,這樣就避免了頻繁調(diào)用系統(tǒng)調(diào)用函數(shù),從而降低內(nèi)存分配的開(kāi)銷(xiāo)。
在 ptmalloc 中,bin主要分為以下四種:
- fast bin
- unsorted bin
- small bin
- large bin
其中根據(jù) bin 的分類(lèi),可以分為 fast bin 和 bins,而 bins 又可以分為 unsorted bin、small bin 以及 large bin 。
fast bin
程序在運(yùn)行時(shí)會(huì)經(jīng)常需要申請(qǐng)和釋放一些較小的內(nèi)存空間。當(dāng)分配器合并了相鄰的幾個(gè)小的 chunk 之后,也許馬上就會(huì)有另一個(gè)小塊內(nèi)存的請(qǐng)求,這樣分配器又需要從大的空閑內(nèi)存中切分出一塊,這樣無(wú)疑是比較低效的,故而, malloc 中在分配過(guò)程中引入了 fast bins 。
fast bin 總共有10個(gè),本質(zhì)上就是10個(gè)單鏈表,每個(gè) fast bin 中所包含的 chunk size 以8字節(jié)逐漸遞增,即如果第一個(gè) fast bin 中 chunk size 均為16個(gè)字節(jié),第二個(gè) fast bin 的 chunk size 為24字節(jié),以此類(lèi)推,最后一個(gè) fast bin 的 chunk size 為80字節(jié)。值得注意的是 fast bin 中 chunk 釋放并不會(huì)與相鄰的空閑 chunk 合并,這是由于 fast bin 設(shè)計(jì)的初衷就是小內(nèi)存的快速分配和釋放,因此系統(tǒng)將屬于 fast bin 的 chunk 的P(未使用標(biāo)志位)總是設(shè)置為1,這樣即使當(dāng) fast bin 中有某個(gè) chunk 同一個(gè) free chunk 相鄰的時(shí)候,系統(tǒng)也不會(huì)進(jìn)行自動(dòng)合并操作。
malloc 操作:
在 malloc 申請(qǐng)內(nèi)存的時(shí)候,如果申請(qǐng)的內(nèi)存大小范圍在fast bin 以?xún)?nèi),則先在 fast bin 中進(jìn)行查找,如果 fast bin 中存在空閑 chunk 則返回。否則依次從 small bin、unsorted bin、large bin 中進(jìn)行查找。
free 操作:
先通過(guò) chunksize 函數(shù)根據(jù)傳入的地址指針獲取該指針對(duì)應(yīng)的 chunk 的大小;然后根據(jù)這個(gè) chunk 大小獲取該 chunk 所屬的 fast bin,然后再將此 chunk 添加到該 fast bin 的鏈尾。
unsorted bin
是 bins 的緩沖區(qū),顧名思義,unsorted bin 中的 chunk 無(wú)序,這種設(shè)計(jì)能夠讓 glibc 的 malloc 機(jī)制有第二次機(jī)會(huì)重新利用最近釋放的 chunk 從而加快內(nèi)存分配的時(shí)間。
與 fast bin 不同,unsorted bin 采用的是 FIFO 的方式。
malloc 操作:
當(dāng)需要的內(nèi)存大小大于 fast bin 的最大大小,則先在 unsorted 中尋找,如果找到了合適的 chunk 則直接返回,否則繼續(xù)在 small bin 和l arge bin中搜索。
free 操作:
當(dāng)釋放的內(nèi)存大小大于fast bin的最大大小,則將釋放的 chunk 寫(xiě)入 unsorted bin。
small bin
大小小于512字節(jié)的 chunk 被稱(chēng)為 small chunk,而保存 small chunks 的 bin 被稱(chēng)為 small bin。62個(gè) small bin 中,每個(gè)相鄰的的 small bin 之間相差8字節(jié),同一個(gè) small bin 中的 chunk 擁有相同大小。
small bin 指向的是包含空閑區(qū)塊的雙向循環(huán)鏈表。內(nèi)存分配和釋放邏輯如下:
malloc 操作:
當(dāng)需要的內(nèi)存不存在于 fast bin 和 unsorted bin 中,并且大小小于512字節(jié),則在 small bin 中進(jìn)行查找,如果找到了合適的 chunk 則直接返回。
free 操作:
free 一個(gè) chunk 時(shí)會(huì)檢查該 chunk 相鄰的 chunk 是否空閑,如果空閑則需要先合并,然后將合并的 chunk 先從所屬的鏈表中刪除然后合并成一個(gè)新的 chunk,新的 chunk 會(huì)被添加在 unsorted bin 鏈表的前端。
large bin
大小大于等于512字節(jié)的 chunk 被稱(chēng)為 large chunk,而保存 large chunks 的 bin 被稱(chēng)為 large bin。large bins 中每一個(gè) bin 分別包含了一個(gè)給定范圍內(nèi)的 chunk,其中的 chunk 按大小遞減排序,大小相同則按照最近使用時(shí)間排列。63 large bin 中的每一個(gè)都與 small bin 的操作方式大致相同,但不是存儲(chǔ)固定大小的塊,而是存儲(chǔ)大小范圍內(nèi)的塊。每個(gè) large bin 的大小范圍都設(shè)計(jì)為不與 small bin 的塊大小或其他large bin 的范圍重疊。
malloc 操作:
首先確定用戶(hù)請(qǐng)求的大小屬于哪一個(gè) large bin,然后判斷該 large bin 中最大的 chunk 的 size 是否大于用戶(hù)請(qǐng)求的 size。如果大于,就從尾開(kāi)始遍歷該 large bin,找到第一個(gè) size 相等或接近的 chunk,分配給用戶(hù)。如果該 chunk 大于用戶(hù)請(qǐng)求的 size 的話,就將該 chunk 拆分為兩個(gè) chunk:前者返回給用戶(hù),且 size 等同于用戶(hù)請(qǐng)求的 size;剩余的部分做為一個(gè)新的 chunk 添加到 unsorted bin 中。
free 操作:
large bin 的 fee 操作與 small bin 一致,此處不再贅述。
3.3.4 特殊 chunk
top chunk
top chunk 是堆最上面的一段空間,它不屬于任何 bin,當(dāng)所有的 bin 都無(wú)法滿(mǎn)足分配要求時(shí),就要從這塊區(qū)域里來(lái)分配,分配的空間返回給用戶(hù),剩余部分形成新的 top chunk,如果 top chunk 的空間也不滿(mǎn)足用戶(hù)的請(qǐng)求,就要使用 brk 或者 mmap 來(lái)向系統(tǒng)申請(qǐng)更多的堆空間(主分配區(qū)使用 brk、sbrk,非主分配區(qū)使用 mmap)。
mmaped chunk
當(dāng)分配的內(nèi)存非常大(大于分配閥值,默認(rèn)128K)的時(shí)候需要被 mmap 映射,則會(huì)放到 mmaped chunk 上,釋放 mmaped chunk 上的內(nèi)存的時(shí)候會(huì)將內(nèi)存直接交還給操作系統(tǒng)。(chunk 中的M標(biāo)志位置1)
last remainder chunk
如果用戶(hù)申請(qǐng)的 size 屬于 small bin 的,但是又不能精確匹配的情況下,這時(shí)候采用最佳匹配(比如申請(qǐng)128字節(jié),但是對(duì)應(yīng)的bin是空,只有256字節(jié)的 bin 非空,這時(shí)候就要從256字節(jié)的 bin 上分配),這樣會(huì) split chunk 成兩部分,一部分返給用戶(hù),另一部分形成 last remainder chunk,插入到 unsorted bin 中。
3.3.5 hunk 的合并與切分
合并
當(dāng) chunk 釋放時(shí),如果前后兩個(gè)相鄰的 chunk 均空閑,則會(huì)與前后兩個(gè)相鄰 chunk 合并,隨后將合并結(jié)果放入 unsorted bin 中。
切分
當(dāng)需要分配的內(nèi)存小于待分配的 chunk 塊,則會(huì)將待分配 chunk 塊切割成兩個(gè) chunk 塊,其中一個(gè) chunk 塊大小等同于用戶(hù)需要分配內(nèi)存的大小。需要注意的是分裂后的兩個(gè) chunk 必須均大于 chunk 的最小大小,否則不會(huì)進(jìn)行拆分。
3.4 內(nèi)存分配
內(nèi)存分配流程可以分為三步:
第一步:根據(jù)用戶(hù)請(qǐng)求大小轉(zhuǎn)換為實(shí)際需要分配 chunk 空間的大??;
第二步:在 bins 中搜索還沒(méi)有歸還給操作系統(tǒng)的 chunk 塊,具體流程如下圖所示。
- 如果所需分配的 chunk 大小小于等于 max_fast (fast bins 中要求的最大 chunk 大小,默認(rèn)為64B),則嘗試在 fast bins 中獲取 chunk,如果獲取 chunk 則返回。否則進(jìn)入下一步。
- 判斷所需大小是否可能處于 small bins 中,即判斷 chunk_size < 512B是否成立。如果 chunk 大小處在 small bins 中則在 small bins 中搜索合適的 chunk,即找到合適的 small bin,然后從該 bin 的尾部摘取一個(gè)滿(mǎn)足大小要求的 chunk 返回。如果 small bins 中無(wú)法找到合適的 chunk 則進(jìn)入下一步。
- 到這一步說(shuō)明待分配的內(nèi)存塊要么是一個(gè)大的 chunk,要么只是沒(méi)有在 small bin 中找到。分配器先在 fast bin 中嘗試合并 chunk,并將 chunk 寫(xiě)入 unsorted chunk 中,此時(shí)再遍歷 unsorted chunk 如果能夠找到合適的 chunk 則按需將該 chunk 切分(可能不需要),將生成的 chunk 中其中一個(gè)放入 small bins 或者 large bins 中,另一個(gè)與待分配內(nèi)存塊相同大小的 chunk 則返回。
- 在 large bins 中搜索合適的 chunk,如果能夠找到則將該 chunk 切分成需要分配的內(nèi)存大小,另一部分則繼續(xù)寫(xiě)入 bins 中。如果無(wú)法找到合適的 chunk,則進(jìn)入下一步。
- 嘗試從 top chunk 中分配一塊內(nèi)存給用戶(hù),剩下一部分生成新的 top chunk 。
第三步:如果 top chunk 依然無(wú)法滿(mǎn)足分配請(qǐng)求,通過(guò) sbrk 或 mmap 增加 top chunk 的大小并分配內(nèi)存給用戶(hù)。
3.5 內(nèi)存釋放
- 判斷當(dāng)前 chunk 是否是 mmap 映射區(qū)域映射的內(nèi)存,如果是則直接使用 munmap 釋放這塊內(nèi)存映射(內(nèi)存映射的內(nèi)存能夠通過(guò)標(biāo)記進(jìn)行識(shí)別);
- 判斷 chunk 是否與 top chunk 相鄰,如果相鄰則直接與 top chunk 合并;
- 如果 chunk 的大小大于 max_fast(64B),則將其放入 unsorted bin,
并檢查是否有合并,如果能夠合并則將 chunk 合并后根據(jù)大小加入合適的 bin 中; - 如果 chunk 的大小小于
max_fast(64B),則直接放入 fast bin 中,如果沒(méi)有合并情況則 free 內(nèi)存。如果在當(dāng)前 chunk 相鄰的 chunk 空閑,則觸發(fā)合并,并將合并后的結(jié)果寫(xiě)入 unsorted bin 中,此時(shí)如果合并后的結(jié)果大于 max_fast(64B),則觸發(fā)整個(gè) fast bins 的合并操作,此時(shí) fast bins 將會(huì)被遍歷,將所有相鄰的空閑 chunk 進(jìn)行合并,然后將合并后的 chunk 寫(xiě)入 unsorted bin 中,fast bin 此時(shí)會(huì)變?yōu)榭?。如果合并后?chunk 與 top chunk 相鄰則會(huì)合并到 top chunk 中; - 如果 top chunk 大小大于 mmap 收縮閾值(默認(rèn)128KB),如果是,則對(duì)于主分配區(qū)則會(huì)試圖歸還 top chunk 中一部分給操作系統(tǒng),此時(shí) free 結(jié)束。
3.6 內(nèi)存碎片
按照 glibc 的內(nèi)存分配策略,我們考慮下如下場(chǎng)景:
1.假設(shè) brk 起始地址為512k
2.malloc 40k 內(nèi)存,即 chunk A,brk = 512k + 40k = 552k
3.malloc 50k 內(nèi)存,即 chunk B,brk = 552k + 50k = 602k
4.malloc 60k 內(nèi)存,即 chunk C,brk = 602k + 60k = 662k
5.free chunk A。
此時(shí) chunk A 為空閑塊,但是如果 chunk C 和 chunk B 一直不釋放無(wú)法直接通過(guò)移動(dòng)brk指針來(lái)釋放 chunk A 的內(nèi)存,必須等待 chunk B 和 chunk C 釋放才能和 top chunk 合并并將內(nèi)存歸還給操作系統(tǒng)。
四、問(wèn)題分析與解決
通過(guò)前面的內(nèi)存分配器運(yùn)行原理能夠很容易得出原因,由于程序中連續(xù)調(diào)用 free/delete 釋放內(nèi)存僅僅只是將內(nèi)存寫(xiě)入內(nèi)存分配器的 bins 中,并沒(méi)有將其歸還給操作系統(tǒng),所以會(huì)出現(xiàn)疑似內(nèi)存未回收的情況。并且如果每次 delete 的內(nèi)存都不與 top chunk 相鄰,會(huì)導(dǎo)致 chunk 塊長(zhǎng)時(shí)間留在空閑鏈表中無(wú)法合并到 top chunk,從而出現(xiàn)內(nèi)存無(wú)法釋放給操作系統(tǒng)的現(xiàn)象。
4.1 優(yōu)化辦法
- 通過(guò)限制服務(wù)端內(nèi)存最大大小能夠有效避免內(nèi)存被c運(yùn)行庫(kù)撐的太高,導(dǎo)致服務(wù)器 OOM 的情況。
- c運(yùn)行庫(kù)替換成 jemalloc,jemalloc 與 glibc 的實(shí)現(xiàn)方式不同,能夠更快將內(nèi)存歸還給操作系統(tǒng)。
4.2 效果對(duì)比測(cè)試
為了驗(yàn)證優(yōu)化后的內(nèi)存使用效果,編寫(xiě)測(cè)試代碼,模擬線上 pipline 模式下的3000萬(wàn)次連續(xù)請(qǐng)求,對(duì)比請(qǐng)求過(guò)程中的內(nèi)存峰值、連接斷開(kāi)后的內(nèi)存使用狀況:
glibc內(nèi)存分配器
內(nèi)存峰值
連接斷開(kāi)后內(nèi)存占用
jemalloc內(nèi)存分配器
內(nèi)存峰值
連接斷開(kāi)后內(nèi)存占用
根據(jù)測(cè)試結(jié)果,jemalloc 相較于 glibc 釋放空閑內(nèi)存速度快12%。
參考鏈接