直接上代碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int alloc_time = 4000;
char *a[alloc_time];
char *b[alloc_time];
int i, j;
for(i=0; i<alloc_time; i++)
{
a[i] = (char *)malloc(52722);
memset(a[i], 1, 52722);
b[i] = (char *)malloc(1);
memset(b[i], 1, 1);
}
printf("malloc finished\n");
for(i=alloc_time-1; i>=0; i--)
{
free(a[i]);
free(b[i]);
}
printf("free finished\n");
//char *p = (char *)malloc(2000);
//free(p);
while(1){
sleep(3);
}
}
運行這個測試程序,發現Glibc內存暴增,程序已經把內存返回給了Glibc庫,但Glibc庫卻沒有把內存歸還給操作系統。
分析:
ptmalloc使用chunk結構來實現內存管理。用戶free掉的內存並不是都會馬上歸還給系統,ptmalloc會統一管理heap和mmap映射區域中的空閒chunk.當用戶進行下一次分配請求時,ptmalloc會首先試圖在空閒的chunk中挑選一塊給用戶,這樣就避免了頻繁的系統調用,降低了內存分配的開銷。對於空閒的chunk,ptmalloc采用分箱式內存管理方式,根據空閒chunk的大小和處於的狀態將其放在三個不同的容器中。
bins:ptmalloc將相似大小的chunk用雙向鏈表鏈接起來,這樣的一個鏈表被稱為一個bin.bins有128個隊列,前64個隊列是定長的(small bins),每隔8個字節大小的塊分配在一個隊列,後面的64個隊列是不定長的(large bins),就是在一個范圍長度的都分配在一個隊列中。所有長度小於512字節的都分配在定長的隊列中,後面的64個隊列是變長的隊列,每個隊列中的chunk都是從大到小排列的。
unsort隊列(只有一個隊列),它是一個cache,所有free下來的如果要進入bins隊列中都要經過unsort隊列,分配內存時會查看unsorted bin中是否有合適的chunk,如果找到滿足條件的chunk,則直接返回給用戶,否則將unsorted bin中所有chunk放入bins中。
fastbins,大約有10個定長隊列,它是一個高速緩沖,所有free下來的並且長度是小於max_fast(默認80B)的chunk就會進入這種隊列中。進入此隊列的chunk在free的時候並不修改使用位,目的是為了避免被相鄰的塊合並掉。
如果內存塊是空閒的,它會掛在其中的一個隊列中,它是通過復用的方式,使用空閒chunk的第3個字和第4個字當作它的前鏈和後鏈(變長塊是第5個字和第6個字)。
malloc的步驟:
1. 先在fastbins中找,如果能找到,從隊列中取下後(不需要再置使用位為1)立刻返回;
2. 判斷需求的塊是否在small bins(bins的前64個bin)范圍,如果在小箱子范圍,並且剛好有滿足需求的塊,則直接返回內存地址;
3. 到了這一步,說明需要分配的是一塊大內存,或者小箱子裡找不到合適的chunk;這個時候,會觸發consolidate,ptmalloc首先會遍歷fastbins中的chunk,將相鄰的chunk合並,並鏈接到unsorted bin中(因為在大箱子找一般都要切割,所以要優先合並,避免過多碎片);
4. 在unsort bin中取出一個chunk,如果能找到剛好和想要的chunk相同大小的chunk,立刻返回,如果不是想要的chunk大小的chunk,就把它插入到bins對應的隊列中去,轉到2.
5. 到了這一步,說明需要分配的是一塊大的內存,或者small bins和unsorted bin中都找不到合適的chunk,並且fastbins和unsorted bin中所有的chunk都清楚干淨了。在large bins中找,找到一個最小的能符合需求的chunk從隊列中取下,如果剩下的大小還能建一個chunk,就把chunk分成兩個部分,把剩下的chunk插入到unsort隊列中取,把chunk的內存地址返回;
6. 如果搜索fastbins和bins都沒有找到合適的chunk,那麼就需要操作topchunk(是堆頂的一個chunk,不會放在任何一個隊列裡)來進行分配了。在topchunk找,如果能切出符合要求的,把剩下的一部分當作topchunk,然後返回內存地址;
7. 到了這一步說明topchunk也不能滿足分配要求,就只能調用sysalloc,其實就是增長堆了,然後返回內存地址。
free的步驟:
1. 判斷所需釋放的chunk是否為mmaped chunk,如果是,則調用munmap釋放mmaped chunk,解除內存空間映射,該空間不再有效,然後立刻返回;
2. 如果和topchunk相鄰,直接和topchunk合並,不會放到其他的空閒隊列中取,然後立刻返回;
3. 如果釋放的大小小於max_fast(80字節),就把它掛到fastbins中去返回,使用位仍然為1,當然更不會去合並相鄰塊,然後立刻返回;
4. 如果釋放塊得大小介於80-128K,把chunk的使用位置為0,判斷前一個chunk是否處於使用中,如果前一塊也是空閒塊,則合並,並轉入下一步;
5. 判斷當前釋放chunk的下一個塊是否為top chunk,如果是,則轉到第7步,否則轉下一步;
6. 判斷下一個chunk是否處在使用中,如果也是空閒的,則合並,並將合並後的chunk掛到unsort隊列中去;
7. 如果執行到了這一步,說明釋放了一個與top chunk相鄰的chunk;則無論它有多大,都將它與top chunk合並,並更新top chunk的大小等信息,轉下一步;
8. 如果合並後的大小大於FASTBIN_CONSOLIDATION_THRESHOLD(64K),也會觸發consolidate,即fastbins的合並操作,合並後的chunk會被放到unsorted bin中,fastbins將變為空,操作完成之後轉下一步;
9. 試圖收縮堆。(判斷topchunk的大小是否大於mmap的收縮阈值,默認為128KB)。
ptmalloc對於大於128K的塊通過mmap方式來分配,小於128K(mmap分配阈值)的塊在heap中分配。堆是通過brk的方式來增加或壓縮的,如果在現有的堆中不能找到合適的chunk,會通過增長堆的方式來滿足分配,如果堆頂的空閒塊超過一定的阈值會收縮堆,所以只要堆頂的空間沒釋放,堆是一直不會收縮的。因為ptmalloc的內存收縮是從top chunk開始,如果與top chunk(堆頂的一個chunk)相鄰的那個chunk在內存池中沒有釋放,top chunk以下的空閒內存都無法返回給系統,即使這些空閒內存有幾十個G也不行。
按照這個測試程序分配後,內存變成由小塊和大塊交替出現,釋放小塊的時候,直接把小塊放在fastbins中取,而且他的使用位還是1,釋放大塊的時候,它試圖合並相鄰的塊,但是和它相鄰的塊的使用位還是1,所以它不能把相鄰的塊合並起來,而且釋放的塊的大小小於64K,也不會觸發consolidate,即不會把fastbins清空,所以當所有的塊都被釋放完後,所有的小塊都在fastbins裡面,使用位都還是1,大塊都掛在unsort隊列裡面。全部都無法合並。所以使用的堆更加無法壓縮。如果在循環後面再分配2000字節然後釋放的話,所有內存將全部被清空,這是因為再申請2000字節的時候,由malloc的第二步,程序會先調用consolidate,即把所有的fastbins清空,同時把相鄰的塊合並起來,等到所有的fastbins清空的時候,所有的塊也被合並起來了,然後調用free(2000)的時候,這塊將被合並起來,成為topchunk,並且大小遠小於64K,所有堆將會壓縮,內存歸還給系統。