malloc內存管理總結

內存管理

內存管理主要包含兩個層面的內容:

  • 1、操作系統內核相關的內存管理:物理內存層
  • 2、庫函數層:主要是堆內存,即malloc實現層

如果用戶還有需要會在用戶層再做一次內存管理機制,例如SGI STL中的內存管理機制(二級配置器)。

由於篇幅有限,本文主要介紹庫函數層的malloc實現機制。同時上述兩層中由於操作系統等不同也存在差異,例如malloc層,Windows下VC++6.0的malloc實現和Linux下用的ptmalloc實現也存在差異,但是所使用的trick是類似的。通常有:1、採用自由鏈表進行維護不同塊的分配與釋放;2、採用內存池的方式,減少對操作系統內核的調用。因此,本文針對ptmalloc2的機制進行分析。

ptmalloc2是支持多線程分配內存的算法,因此將主線程分配區與子線程分配區分開,主線程分配區可通過系統調用brk()和內存映射系統調用mmap()來分配,而子線程分配區只能通過內存映射mmap()的方式獲得內存。

相關數據結構描述:

1. Main_arena與non_main_arena:主分配區和非主分配區

  • ps:ptmalloc爲什麼要增加非主分配區?答:如果沒有非主分配區,所有的線程在主分配區上操作,互相競爭鎖的過程十分影響分配效率。ptmalloc中增加了非主分配區支持,主分配區和非主分配區用環形鏈表進行管理,提高malloc的分配效率。 申請小塊內存時會產生很多內存碎片,ptmalloc在整理時也需要對分配區做加鎖操作。每個加鎖操作大概需要5~10個cpu指令,而且程序線程很多的情況下,鎖等待的時間就會延長,導致malloc性能下降。一次加鎖操作需要消耗100ns左右,正是鎖的緣故,導致ptmalloc在多線程競爭情況下性能遠遠落後於tcmalloc。最新版的ptmalloc對鎖進行了優化,加入了PER_THREAD和ATOMIC_FASTBINS優化,但默認編譯不會啓用該優化,這兩個對鎖的優化應該能夠提升多線程內存的分配的效率。

2.chunk

chunk是用戶所需的內存塊,ptmalloc爲了便於管理在其前後加了一些控制信息,用來記錄分配的信息,以便完成分配和釋放工作。已被分配使用的chunk如圖所示:

chunk指針指向一個chunk的開始,mem指針是真正返回給用戶使用的指針。size of previous chunk和size of chunk(類似於VC++6.0中的上下cookie)用於內存釋放時回收方便,從上往下回收時可以知道下一個塊的起始地址。chunk的第二個域的最低一位爲P,它表示前一個塊是否正在使用,P爲0則表示前一個chunk爲空閒,這時chunk的第一個域pre_size纔有效,pre_size表示前一個chunk的size,程序可以使用這個值來找到前一個chunk的開始地址。Chunk的第二個域的倒數第二個位爲M,他表示當前chunk是從哪個內存區域獲得的虛擬內存。M爲1表示該chunk是從mmap映射區域分配的,否則是從heap區域分配的。Chunk的第二個域倒數第三個位爲A,表示該chunk屬於主分配區或者非主分配區,如果屬於非主分配區,將該位置爲1,否則置爲0。
空閒chunk在內存中的結構如圖所示:

當chunk空閒時,其M狀態不存在,只有AP狀態,原本是用戶數據區的地方存儲了四個指針,指針fd指向後一個空閒的chunk,而bk指向前一個空閒的chunk,ptmalloc通過這兩個指針將大小相近的chunk連成一個雙向鏈表。對於large bin中的空閒chunk,還有兩個指針,fd_nextsize和bk_nextsize,這兩個指針用於加快在large bin中查找最近匹配的空閒chunk。不同的chunk鏈表又是通過bins或者fastbins來組織的(bins和fastbins在後面介紹)。

  • chunk空間複用:是指一個chunk塊或者正在被使用或者已經被釋放掉,所以chunk中的一些域可以在使用狀態和空閒狀態表示不同的意義,來達到空間複用的效果。空閒時,一個chunk至少需要4個size_t(4B)大小的空間,用來存儲prev-size,size,fd,bk,也就是16B。當一個chunk處於使用狀態時,其大小計算公式:in-use-size=(用戶請求大小+8-4)align to 8B,其中+8是用來存儲prev-size和size,但又因爲,該塊處於使用狀態,它的下一個chunk的pre-size域肯定是無效的,所以被借用過來作爲該chunk的空間存儲,所以-4。即最終的分配空間chunk-size=max(in-use-size, 16)。

3.bins

用戶釋放掉的內存chunk並不都是馬上歸還給系統,ptmalloc會統一管理heap和mmap映射區域中的空閒chunk,當用戶進行下一次分配請求時,ptmalloc會首先試圖在空閒的chunk中挑選一塊給用戶,這樣就避免了頻繁的系統調用,降低內存分配的開銷。ptmalloc將相似大小的chunk用雙向鏈表鏈接起來,這樣的鏈表被稱爲一個bin。ptmalloc維護了128個bin,並使用一個數組來存儲這些bin,如圖。

數組中的第一個爲unsorted bin,數組中從2開始編號的前64個bin稱爲small bins,同一個small bin中的chunk具有相同的大小。兩個相鄰的small bin中的chunk大小相差8bytes。small bins中的chunk按照最近使用順序進行排列,最後釋放的chunk被鏈接到鏈表的頭部,而申請chunk是從鏈表尾部開始,這樣,每一個chunk 都有相同的機會被ptmalloc選中。Small bins後面的bin被稱作large bins。large bins中的每一個bin分別包含了一個給定範圍內的chunk,其中的chunk按大小序排列。相同大小的chunk同樣按照最近使用順序排列。ptmalloc使用“smallest-first,best-fit”原則在空閒large bins中查找合適的chunk。
當空閒的chunk被鏈接到bin中的時候,ptmalloc會把表示該chunk是否處於使用中的標誌P設爲0(注意,這個標誌實際上處在下一個chunk中),同時ptmalloc還會檢查它前後的chunk是否也是空閒的,如果是的話,ptmalloc會首先把它們合併爲一個大的chunk,然後將合併後的chunk放到unstored bin中。要注意的是,並不是所有的chunk被釋放後就立即被放到bin中。ptmalloc爲了提高分配的速度,會把一些小的的chunk先放到一個叫做fast bins的容器內。

4.Fast Bins

用戶釋放的較小(不大於max-fast設定值,默認爲64B)的內存空間均被放到fast-bins中,起到一個緩衝的作用。用戶釋放較小內存塊後,首先被放到fast-bins中,並不改變它的使用標誌P,這樣就無法將它們合併,當用戶再次申請小於等於max-fast值時,ptmalloc首先會在fast-bin中查找相應空閒塊,找不到再去找bins(small-bins、unsorted-bins、large-bins)中的空閒塊。在某個特定的時候,ptmalloc會遍歷fast bins中的chunk,將相鄰的空閒chunk進行合併,並將合併後的chunk加入unsorted bin中,然後再將unsorted bin裏的chunk加入bins中。

5.unsorted Bin

unsorted bin的隊列使用bins數組的第一個,如果被用戶釋放的chunk大於max_fast,或者fast bins中的空閒chunk合併後,這些chunk首先會被放到unsorted bin隊列中,在進行malloc操作的時候,如果在fast bins中沒有找到合適的chunk,則ptmalloc會先在unsorted bin中查找合適的空閒chunk,然後才查找bins。如果unsorted bin不能滿足分配要求。malloc便會將unsorted bin中的chunk加入bins中。然後再從bins中繼續進行查找和分配過程。從這個過程可以看出來,unsorted bin可以看做是bins的一個緩衝區,增加它只是爲了加快分配的速度。

6.Top chunk

對於非主分配區會預先從mmap區域分配一塊較大的空閒內存模擬sub-heap,通過管理sub-heap來響應用戶的需求,因爲內存是按地址從低向高進行分配的,在空閒內存的最高處,必然存在着一塊空閒chunk,叫做top chunk。當bins和fast bins都不能滿足分配需要的時候,ptmalloc會設法在top chunk中分出一塊內存給用戶,如果top chunk本身不夠大,分配程序會重新分配一個sub-heap,並將top chunk遷移到新的sub-heap上,新的sub-heap與已有的sub-heap用單向鏈表連接起來,然後在新的top chunk上分配所需的內存以滿足分配的需要,實際上,top chunk在分配時總是在fast bins和bins之後被考慮,所以,不論top chunk有多大,它都不會被放到fast bins或者是bins中。Top chunk的大小是隨着分配和回收不停變換的,如果從top chunk分配內存會導致top chunk減小,如果回收的chunk恰好與top chunk相鄰,那麼這兩個chunk就會合併成新的top chunk,從而使top chunk變大。如果在free時回收的內存大於某個閾值,並且top chunk的大小也超過了收縮閾值,ptmalloc會收縮sub-heap,如果top-chunk包含了整個sub-heap,ptmalloc會調用munmap把整個sub-heap的內存返回給操作系統。
由於主分配區是唯一能夠映射進程heap區域的分配區,它可以通過sbrk()來增大或是收縮進程heap的大小,ptmalloc在開始時會預先分配一塊較大的空閒內存(也就是所謂的 heap),主分配區的top chunk在第一次調用malloc時會分配一塊(chunk_size + 128KB) align 4KB大小的空間作爲初始的heap,用戶從top chunk分配內存時,可以直接取出一塊內存給用戶。在回收內存時,回收的內存恰好與top chunk相鄰則合併成新的top chunk,當該次回收的空閒內存大小達到某個閾值,並且top chunk的大小也超過了收縮閾值,會執行內存收縮,減小top chunk的大小,但至少要保留一個頁大小的空閒內存,從而把內存歸還給操作系統。如果向主分配區的top chunk申請內存,而top chunk中沒有空閒內存,ptmalloc會調用sbrk()將的進程heap的邊界brk上移,然後修改top chunk的大小。

7.mmaped chunk

當需要分配的chunk足夠大,而且fast bins和bins都不能滿足要求,甚至top chunk本身也不能滿足分配需求時,ptmalloc會使用mmap來直接使用內存映射來將頁映射到進程空間。這樣分配的chunk在被free時將直接解除映射,於是就將內存歸還給了操作系統,再次對這樣的內存區的引用將導致segmentation fault錯誤。這樣的chunk也不會包含在任何bin中。

8.Last remainder

Last remainder是另外一種特殊的chunk,就像top chunk和mmaped chunk一樣,不會在任何bins中找到這種chunk。當需要分配一個small chunk,但在small bins中找不到合適的chunk,如果last remainder chunk的大小大於所需的small chunk大小,last remainder chunk被分裂成兩個chunk,其中一個chunk返回給用戶,另一個chunk變成新的last remainder chuk。

考慮線程安全:

每次分配都需要獲得分配區(arena)的鎖,爲了防止多個線程同時訪問同一個分配區,在進行分配之前需要取得分配區的鎖。線程先查看線程私有對象中是否已經存在一個分配區,如果存在嘗試對該分配區加鎖,如果加鎖成功,使用該分配區分配內存,否則,該線程搜索分配區循環鏈表試圖獲得一個空閒(沒有加鎖)的分配區。如果所有分配區均已加鎖,則判斷分配區的個數是否到達系統上限(2*CPU核心數+1),若未達到上限,則新建一個分配區,並把該分配區加入到全局分配區循環鏈表和線程的私有對象中並加鎖,然後使用該分配區進行分配操作。新建的分配區一定是非主分配區,因爲主分配區是從父進程那裏繼承的。新建非主分配區時會調用mmap()創建一個sub-heap,並設置好top-chunk。

分配過程:

  1. ptmalloc在開始時,若請求的空間小於mmap分配閾值(mmap threshold,默認值爲128KB)時,主分配區會調用sbrk()增加一塊大小爲 (128 KB + chunk-size) align 4KB的空間作爲heap,若大於mmap分配閾值,則ptmalloc直接使用mmap()映射一塊大小爲chunk的內存作爲heap。非主分配區會調用mmap映射一塊大小爲HEAP-MAX-SIZE(32位系統上默認爲1MB,64位系統上默認爲64MB)的空間作爲sub-heap。當用戶請求內存分配時,首先會在這個區域找一塊合適的chunk給用戶。當用戶釋放heap中的chunk時,ptmalloc又會使用fast bins和bins來組織空閒chunk。
  2. 若brk!=brk-start,若用戶申請內存,先判斷所需分配chunk的大小是否滿足chunk-size<=max-fast(max-fast默認爲64B),如果是的話則轉到下一步。
  3. 首先嚐試在fast bins中取一個所需大小的chunk分配給用戶。如果可以找到,則分配結束。否則轉到下一步。
  4. 判斷所需大小是否在small bins中,即判斷chunk-size < 512B是否成立。如果chunk大小處在small bins中,則轉下一步,否則轉6步
  5. 根據所需分配的chunk的大小,找到具體所在的某個small bin,從該bin的尾部摘取一個恰好滿足大小的chunk。若成功,則分配結束,否則,轉到下一步。
  6. 到了這一步,說明需要分配的內存較大。ptmalloc首先遍歷fast bins中的chunk,並將相鄰的chunk進行合併,並鏈接到unsorted bin中,然後遍歷unsorted bin中的chunk,如果unsorted bin只有一個chunk,並且這個chunk在上次分配過程中被使用過,並且所需分配的chunk大小屬於small bins,並且chunk的大小大於等於需要分配的大小,這種情況下就直接將該chunk進行切割,分配結束,否則將根據chunk的空間大小將其放入small bins或是large bins中,遍歷完成後,轉入下一步
  7. 到了這一步,說明需要分配的內存較大。或者small bins和unsorted bins中都找不到合適的chunk,並且fast bins和unsorted bins中所有的chunk都清除乾淨了。從large bins中按照“smallest-first,best-fit”原則,找一個合適的chunk,從中劃分一塊所需大小的chunk,並將剩下的部分鏈接回到bins中。若操作成功,則分配結束,否則轉到下一步。
  8. 如果搜索fast bins和bins都沒有找到合適的chunk,那麼就需要操作top chunk來進行分配了。判斷top chunk大小是否滿足所需chunk的大小,如果是,則從top chunk中分出一塊來。否則轉到下一步。
  9. 到了這一步,說明top chunk也不能滿足分配要求,所以,於是就有了兩個選擇: 如果是主分配區,調用sbrk(),增加top chunk大小;如果是非主分配區,調用mmap來分配一個新的sub-heap,增加top chunk大小;或者使用mmap()來直接分配。在這裏,需要依靠chunk的大小來決定到底使用哪種方法。判斷所需分配的chunk大小是否大於等於 mmap分配閾值,如果是的話,則轉下一步,調用mmap分配,否則跳到第11步,增加top chunk 的大小。
  10. 使用mmap系統調用爲程序的內存空間映射一塊chunk_size align 4kB大小的空間。 然後將內存指針返回給用戶。
  11. 判斷是否爲第一次調用malloc,若是主分配區,則需要進行一次初始化工作,分配一塊大小爲(chunk_size + 128KB) align 4KB大小的空間作爲初始的heap。若已經初始化過了,主分配區則調用sbrk()增加heap空間,非主分配區則在top chunk中切割出一個chunk,使之滿足分配需求,並將內存指針返回給用戶。

釋放過程

  1. 判斷傳入的指針是否爲0,如果爲0,則什麼都不做,直接return。否則轉下一步。
  2. 判斷所需釋放的chunk是否爲mmaped chunk,如果是,則調用munmap()釋放mmaped chunk,解除內存空間映射,該該空間不再有效,訪問該區域會報錯。如果開啓了mmap分配閾值的動態調整機制,並且當前回收的chunk大小大於mmap分配閾值,將mmap分配閾值設置爲該chunk的大小,將mmap收縮閾值設定爲mmap分配閾值的2倍(??沒看懂爲什麼),釋放完成,否則跳到下一步。
  3. 判斷chunk的大小和所處的位置,若chunk_size <= max_fast,並且chunk並不位於heap的頂部,也就是說並不與top chunk相鄰,則轉到下一步,否則跳到第5步。(因爲與top chunk相鄰的小chunk也和 top chunk進行合併,所以這裏不僅需要判斷大小,還需要判斷相鄰情況)
  4. 將chunk放到fast bins中,chunk放入到fast bins中時,並不修改該chunk使用狀態位P。也不與相鄰的chunk進行合併。只是放進去,如此而已。這一步做完之後釋放便結束了,程序從free()函數中返回。
  5. 判斷前一個chunk是否處在使用中,如果前一個塊也是空閒塊,則合併。並轉下一步。
  6. 判斷當前釋放chunk的下一個塊是否爲top chunk,如果是,則轉第8步,否則轉下一步。
  7. 判斷下一個chunk是否處在使用中,如果下一個chunk也是空閒的,則合併,並將合併後的chunk放到unsorted bin中。注意,這裏在合併的過程中,要更新chunk的大小,以反映合併後的chunk的大小。並轉到第9步。
  8. 如果執行到這一步,說明釋放了一個與top chunk相鄰的chunk。則無論它有多大,都將它與top chunk合併,並更新top chunk的大小等信息。轉下一步。
  9. 判斷合併後的chunk 的大小是否大於FASTBIN-CONSOLIDATION-THRESHOLD(默認64KB),如果是的話,則會觸發進行fast bins的合併操作,fast bins中的chunk將被遍歷,並與相鄰的空閒chunk進行合併,合併後的chunk會被放到unsorted bin中。fast bins將變爲空,操作完成之後轉下一步。
  10. 判斷top chunk的大小是否大於mmap**收縮閾值(默認爲128KB),如果是的話,對於主分配區,則會試圖歸還top chunk中的一部分給操作系統。判斷top chunk的大小是否大於mmap收縮閾值(默認爲128KB),如果是的話,對於主分配區,則會試圖歸還top chunk中的一部分給操作系統。但是最先分配的128KB空間是不會歸還的,ptmalloc 會一直管理這部分內存,用於響應用戶的分配請求;如果爲非主分配區,會進行sub-heap收縮,將top chunk的一部分返回給操作系統,如果top chunk爲整個sub-heap,會把整個sub-heap還回給操作系統。做完這一步之後,釋放結束,從 free() 函數退出。可以看出,收縮堆的條件是當前free的chunk大小加上前後能合併chunk的大小大於64k,並且要top chunk的大小要達到mmap收縮閾值,纔有可能收縮堆。,ptmalloc 會一直管理這部分內存,用於響應用戶的分配請求;如果爲非主分配區,會進行sub-heap收縮,將top chunk的一部分返回給操作系統,如果top chunk爲整個sub-heap,會把整個sub-heap還回給操作系統。**做完這一步之後,釋放結束,從 free() 函數退出。可以看出,收縮堆的條件是當前free的chunk大小加上前後能合併chunk的大小大於64k,並且要top chunk的大小要達到mmap收縮閾值,纔有可能收縮堆。

使用注意事項:避免內存暴增問題

爲了避免Glibc內存暴增,使用時注意以下幾點:

  • 後分配的內存先釋放(堆特性),防止內存泄漏,因爲ptmalloc收縮內存是從top chunk開始,如果與top-chunk相鄰的chunk不能釋放,top-chunk以下的chunk都無法釋放,針對heap而言。因爲mmap分配的內存都可以單獨釋放。圖中chunk A、chunk D之間的chunk雖然釋放了,但是top chunk並不能收縮該chunk;等chunk D釋放後,才能被top-chunk釋放。
  • Ptmalloc不適合用於管理長生命週期的內存,特別是持續不定期分配和釋放長生命週期的內存,這將導致ptmalloc內存暴增。長時間不釋放的內存將佔據top-chunk導致無法回收給操作系統。所以通過長生命週期的內存經常大於1MB進行申請,這樣可以保證是通過mmap來進行分配,回收時可直接回收,而不用等待其他chunk的回收。
  • 儘量使用mmap分配閾值動態調整機制(ps:何時調整動態閾值?:在釋放mmap區域的內存時,並且當前釋放的chunk大小大於mmap分配閾值,則將mmap分配閾值設置爲該chunk的大小,將mmap收縮閾值設定爲mmap分配閾值的2倍。默認情況下mmap分配閾值與mmap收縮閾值相等,都爲128KB。在64位系統上,mmap分配閾值最大值爲32MB,所以收縮閾值的最大值爲64MB,在32位系統上,mmap分配閾值最大值爲512KB,所以收縮閾值的最大值爲1MB。收縮閾值可以通過函數mallopt()進行設置。),這樣可以保證短生命週期的內存分配儘量從ptmalloc緩存的內存chunk中分配,更高效,浪費更少的物理內存。如果關閉了該機制,對大於128KB的內存分配就會使用系統調用mmap向操作系統分配內存,使用系統調用分配內存一般會比從ptmalloc緩存的chunk中分配內存慢,特別是在多線程同時分配大內存塊時,操作系統會串行調用mmap(),併爲發生缺頁異常的頁加載新物理頁時,默認強制清0。頻繁使用mmap向操作系統分配內存是相當低效的。使用mmap分配的內存只適合長生命週期的大內存塊。
  • 多線程分階段執行的程序不適合用ptmalloc,這種程序的內存更適合用內存池管理,就像Appach那樣,每個連接請求處理分爲多個階段,每個階段都有自己的內存池,每個階段完成後,將相關的內存就返回給相關的內存池。Google的許多應用也是分階段執行的,他們在使用ptmalloc也遇到了內存暴增的相關問題,於是他們實現了TCMalloc來代替ptmalloc,TCMalloc具有內存池的優點,又有垃圾回收的機制,並最大限度優化了鎖的爭用,並且空間利用率也高於ptmalloc。Ptmalloc假設了線程A釋放的內存塊能在線程B中得到重用,但B不一定會分配和A線程同樣大小的內存塊,於是就需要不斷地做切割和合並,可能導致內存碎片。
  • 儘量減少程序的線程數量和避免頻繁分配/釋放內存,Ptmalloc在多線程競爭激烈的情況下,首先查看線程私有變量是否存在分配區,如果存在則嘗試加鎖,如果加鎖不成功會嘗試其它分配區,如果所有的分配區的鎖都被佔用着,就會增加一個非主分配區供當前線程使用。由於在多個線程的私有變量中可能會保存同一個分配區,所以當線程較多時,加鎖的代價就會上升,ptmalloc分配和回收內存都要對分配區加鎖,從而導致了多線程競爭環境下ptmalloc的效率降低。

Glibc內存暴增的原因:

  • Ptmalloc不擅長管理長生命週期的內存塊,ptmalloc設計的假設中就明確假設緩存的內存塊都用於短生命週期的內存分配,因爲ptmalloc的內存收縮是從top chunk開始,如果與top chunk相鄰的那個chunk在我們NoSql的內存池中沒有釋放,top chunk以下的空閒內存都無法返回給系統,即使這些空閒內存有幾十個G也不行。
  • Glibc內存暴增的問題我們定位爲全局內存池中的內存塊長時間沒有釋放,其中還有一個原因就是全局內存池會不定期的分配內存,可能下次分配的內存是在top chunk分配的,分配以後又短時間不釋放,導致top chunk升到了一個更高的虛擬地址空間,從而使ptmalloc中緩存的內存塊更多,但無法返回給操作系統。
  • 另一個原因就是進程的線程數越多,在高壓力高併發環境下,頻繁分配和釋放內存,由於分配內存時鎖爭用更激烈,ptmalloc會爲進程創建更多的分配區,由於我們的全局內存池的長時間不釋放內存的緣故,會導致ptmalloc緩存的chunk數量增長得更快,從而更容易重現Glibc內存暴增的問題。在我們的ms上這個問題最爲突出,就是這個原因。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章