TCMalloc是專門對多線並發的內存管理而設計的,TCMalloc主要是在線程級實現了緩存,使得用戶在申請內存時大多情況下是無鎖內存分配。整個 TCMalloc對小內存(小於等於256k)的管理實現了三級緩存,分別是ThreadCache(線程級緩存),Central Cache(中央緩存:CentralFreeeList),PageHeap(頁緩存)。小內存的分配和釋放流程如下圖所示,紅線表示內存的申請,藍線表示內存的釋放過程。下面將分別介紹各級緩存模塊的實現。

一. SizeMap
在介紹三個緩存模塊之前,先需要介紹一下sizeMap。
TCMalloc為了提高內存分配效率和減少內存的浪費,對小內存進行了細化分類,在默認的情況下:
size在(0, 16)之間時,以8字節對齊分配內存,size在[16,128)之間,按16字節對齊來分配內存,size在[128,256*1024),按(2^(n+1)-2^n)/8字節對齊來分配內存(n的值為log2(size)取整,見函數AlignmentForSize())。
TCMalloc對這些細化分類構建了兩個映射表,即class_array_[kClassArraySize]和class_to_size_[kNumClasses]。class_array_[kClassArraySize]表示了size到class的映射關系(size需要先經過函數
ClassIndex(size)轉換),class_to_size_[kNumClasses]表示了class到size的映射關系。要申請一個size的內存時,先從class_array_[
ClassIndex(size)]查到size對應的sizeclass,然后從映射表class_to_size_[kNumClasses]獲取實際獲取的內存大小。
同時TCMalloc另外維護了兩種映射表:class_to_pages_[kNumClasses]和num_objects_to_move_[kNumClasses]。class_to_pages_[kNumClasses]表示了Central Cache每次從PageHeap獲取內存時,對應的sizeclass每次需要從PageHeap獲取幾頁內存;num_objects_to_move_[kNumClasses]表示了ThreadCache每次從Central Cache獲取內存時,對應的sizeclass每次需要從Central Cache獲取的buffer(Object)個數。
下面整個表表示了以8k為一頁時,上述映射表的內容(由於class_array_[kClassArraySize]比較大,這里就不羅列了)。
sizeclass |
class_to_size_ |
class_to_pages_ |
num_objects_to_move_ |
0 |
0 |
0 |
0 |
1 |
8 |
2 |
8192 |
2 |
16 |
2 |
4096 |
3 |
32 |
2 |
2048 |
4 |
48 |
2 |
1365 |
5 |
64 |
2 |
1024 |
6 |
80 |
2 |
819 |
7 |
96 |
2 |
682 |
8 |
112 |
2 |
585 |
9 |
128 |
2 |
512 |
10 |
144 |
2 |
455 |
11 |
160 |
2 |
409 |
12 |
176 |
2 |
372 |
13 |
192 |
2 |
341 |
14 |
208 |
2 |
315 |
15 |
224 |
2 |
292 |
16 |
240 |
2 |
273 |
17 |
256 |
2 |
256 |
18 |
288 |
2 |
227 |
19 |
320 |
2 |
204 |
20 |
352 |
2 |
186 |
21 |
384 |
2 |
170 |
22 |
416 |
2 |
157 |
23 |
448 |
2 |
146 |
24 |
480 |
2 |
136 |
25 |
512 |
2 |
128 |
26 |
576 |
2 |
113 |
27 |
640 |
2 |
102 |
28 |
704 |
2 |
93 |
29 |
768 |
2 |
85 |
30 |
832 |
2 |
78 |
31 |
896 |
2 |
73 |
32 |
960 |
2 |
68 |
33 |
1024 |
2 |
64 |
34 |
1152 |
2 |
56 |
35 |
1280 |
2 |
51 |
36 |
1408 |
2 |
46 |
37 |
1536 |
2 |
42 |
38 |
1792 |
2 |
36 |
39 |
2048 |
2 |
32 |
40 |
2304 |
2 |
28 |
41 |
2560 |
2 |
25 |
42 |
2816 |
3 |
23 |
43 |
3072 |
2 |
21 |
44 |
3328 |
3 |
19 |
45 |
4096 |
2 |
16 |
46 |
4608 |
3 |
14 |
47 |
5120 |
2 |
12 |
48 |
6144 |
3 |
10 |
49 |
6656 |
5 |
9 |
50 |
8192 |
2 |
8 |
51 |
9216 |
5 |
7 |
52 |
10240 |
4 |
6 |
53 |
12288 |
3 |
5 |
54 |
13312 |
5 |
4 |
55 |
16384 |
2 |
4 |
56 |
20480 |
5 |
3 |
57 |
24576 |
3 |
2 |
58 |
26624 |
7 |
2 |
59 |
32768 |
4 |
2 |
60 |
40960 |
5 |
2 |
61 |
49152 |
6 |
2 |
62 |
57344 |
7 |
2 |
63 |
65536 |
8 |
2 |
64 |
73728 |
9 |
2 |
65 |
81920 |
10 |
2 |
66 |
90112 |
11 |
2 |
67 |
98304 |
12 |
2 |
68 |
106496 |
13 |
2 |
69 |
114688 |
14 |
2 |
70 |
122880 |
15 |
2 |
71 |
131072 |
16 |
2 |
72 |
139264 |
17 |
2 |
73 |
147456 |
18 |
2 |
74 |
155648 |
19 |
2 |
75 |
163840 |
20 |
2 |
76 |
172032 |
21 |
2 |
77 |
180224 |
22 |
2 |
78 |
188416 |
23 |
2 |
79 |
196608 |
24 |
2 |
80 |
204800 |
25 |
2 |
81 |
212992 |
26 |
2 |
82 |
221184 |
27 |
2 |
83 |
229376 |
28 |
2 |
84 |
237568 |
29 |
2 |
85 |
245760 |
30 |
2 |
86 |
253952 |
31 |
2 |
87 |
262144 |
32 |
2 |
二. 線程緩存ThreadCache
ThreadCache是線程緩存的對象,每個線程都有一個ThreadCache對象作為本線程的內存緩存池,當線程需要申請內存時,就從自己的ThreadCache中獲取。所有線程的ThreadCache通過雙向鏈表(next_,prev_)連接起來,鏈表頭是靜態變量ThreadCache::thread_heaps_ 。
線程對本線程的ThreadCache的訪問采用線程私有數據的接口進行訪問,線程的私有數據有兩種實現方式:
1. 靜態局部緩存線程私有數據,通過關鍵字static __thread定義一個靜態變量,這個在TCMalloc由編譯宏HAVE_TLS來控制;
2. 動態線程私有數據,通過POSIX接口pthread_key_create,pthread_setspecific,pthread_getspecific來實現。
靜態局部緩存的優點是設置和讀取的速度非常快,比動態方式快很多,但是也有它的缺點。
主要有如下兩個缺點:
1. 靜態緩存在線程結束時沒有辦法清除;而動態線程私有數據在創建key時,就可以注冊釋放線程數據的接口(TCMalloc注冊的接口DestroyThreadCache),當線程退出時會調用這個接口對線程的私有數據進行清理,即可以釋放掉線程申請的資源。
2. 不是所有的操作系統都支持。
tcmalloc采用的是動態局部緩存,但同時檢測系統是否支持靜態方式,如果支持那么同時保存一份拷貝,方便快速讀取。

ThreadCache的實現
ThreadCache的實現比較簡單,如上圖所示,在ThreadCache的對象中保存一個FreeList list_[kNumClasses]數組,數組的每個元素為FreeList,用於管理某個sizeclass的所有緩存,TCMalloc沒有為這些Object設計專用的鏈表來管理,而是將Buffer頭上的4字節或8字節(根據系統而定)用於保存下一個Object的起始地址。鏈表的結構如下圖。

1. 內存分配
TCMalloc分配內存的接口是void* tc_malloc(size_t size);TCMalloc分配內存比較簡單,當一個線程需要一塊size大小的內存時:
1). 線程調用POXIS的線程私有數據接口獲取自己的ThreadCache對象(如果不存在,則創建一個並和線程私有數據的key綁定);
2). 根據SizeMap中的映射表計算出size對應的sizeclass;
3). 根據sizeclass中 list_[kNumClasses]對應的鏈表中查找是否有Object存在,如果有則將頭部的第一個從鏈表中取出返回給用戶;
4). 如果沒有,則從sizeclass對應的Central Cache的CentralFreeList中申請一定數量的Object插入到ThreadCache對應的鏈表中,並將第一個Object返回給用戶。
TCMalloc為了提高內存分配的效率,一次從Central Cache申請一定數量的Object到ThreadCache中,一次申請的數量由映射表num_objects_to_move_[kNumClasses]確定。
2. 內存釋放
TCMalloc釋放內存的接口是void tc_free(void* ptr);接口的參數是釋放內存的首地址:
1). 線程調用POXIS的線程私有數據接口獲取自己的ThreadCache對象;
2). 首先,會計算出ptr在系統內存的哪頁,即PageID(PageID = (ptr) >> kPageShift);
3). 然后調用PageHeap的接口GetSizeClassIfCached(PageID p)從映射表pagemap_cache_得到該頁被哪個sizeclass的所使用的(因為TCMalloc申請緩存時是按頁來申請的),pagemap_cache_映射表會在PageHeap章節描述;
4). 將ptr放到了ThreadCache的 list_[kNumClasses]對應的鏈表頭部;
5). 如果ptr對應的sizeclass的鏈表長度已經超過了鏈表設定的最大長度(最大長度在運行過程中會稍微變化,最好是num_objects_to_move_[kNumClasses]中設定的整數倍),則將num_objects_to_move_[kNumClasses]個Object歸還給對應的Central Cache。
6). 如果整個ThreadCache緩存的內存(ThreadCache::size_)大於本ThreadCache設定的最大緩存(ThreadCache::max_size_,max_size_在運行中根據緩存使用也是可以變化的,即可以減少其他ThreadCache的最大緩存,增加自己的最大緩存,見函數IncreaseCacheLimitLocked())時,即啟動內存回收機制,釋放每個sizeclass鏈表的一半(lowater_/2)的Objects返回給Central Cache。注意,返還的時候,如果Objects鏈表長度大於num_objects_to_move_[kNumClasses],則分多次,每次都是num_objects_to_move_[kNumClasses]個Objects的方式返還給Central Cache,原因是Central Cache有兩個地方緩存Buffer,會根據返回Objects的數量存放不同的地方,具體分析在Central Cache的實現章節描述。
三. 中央緩存Central Cache
Central Cache是所有線程共享的緩沖區,因此對Central Cache的訪問需要加鎖。
Central Cache所有的數據都在Static::central_cache_[kNumClasses]中,即采用數組的方式來管理所有的Cache,每個sizeclass的Central Cache對應一個數組元素,所以對不同sizeclass的的Central Cache的訪問是不沖突的。對Cache掛到管理主要是有類CentralFreeList來實現,數據結構如下圖所示。

CentralFreeList的實現
CentralFreeList是用來在ThreadCache和PageHeap之間緩存Buffer的地方,CentralFreeList有兩個地方來緩存Buffer,它們分別是TCEntry tc_slots_[kMaxNumTransferEntries]和Span nonempty_。
1. TCEntry tc_slots_[kMaxNumTransferEntries]
tc_slots_[kMaxNumTransferEntries]是用來緩存那些從ThreadCache返還Buffer,只有一次返還num_objects_to_move_[kNumClasses]個Object的那些緩存保存在tc_slots_[kMaxNumTransferEntries],因此TCEntry鏈表的長度都是num_objects_to_move_[kNumClasses],這也是為什么前面描述的ThreadCache返回Object長度大於num_objects_to_move_[kNumClasses],則分多次,每次都是於num_objects_to_move_[kNumClasses]個。TCEntry是鏈表的頭,記錄了鏈表的頭尾。這樣在ThreadCache和CentralFreeList就能快速的移動Object,每次從ThreadCache返回Object給CentralFreeList時,直接將它掛到TCEntry上,ThreadCache從CentralFreeList申請內存也類似,可以將鏈表直接插入到ThreadCache的鏈表頭。 ThreadCache每次申請內存時首先從 tc_slots_[kMaxNumTransferEntries]中找,如果有就直接從 tc_slots_[kMaxNumTransferEntries]獲取,否則采用才從nonempty_管理的Span中獲取。
2. Span nonempty_
Span nonempty_主要充當CentralFreeList從PageHeap獲取Buffer的緩存,當CentralFreeList的緩存不夠時,CentralFreeList從PageHeap申請一定量的Buffer(一次獲取的頁數有映射表class_to_pages_[kNumClasses]來確定),先緩存在Span nonempty_中,然后ThreadCache在到Span nonempty_中獲取。當ThreadCache一次返還的Object不是num_objects_to_move_[kNumClasses]個或者tc_slots_[kMaxNumTransferEntries]滿時,才會將Object緩存到Span nonempty_中。
3. Span empty_
Spen empty_主要是用來保存那些Span下面不再有Objects的Span節點。
4. Span
Span是用於管理連續的內存頁,如下圖所示,Page1和Page2屬於Span a,Page3, Page4, Page5和Page6屬於Span b,等等。這些映射關系維護在PageHeap的PageMap pagemap_中,關於PageMap pagemap_將在PageHeap中描述。

需要注意的是Span在PageHeap和CentralFreeList是不同的。
在PageHeap中的Span只是對它管理的連續內存的第一頁和最后一頁在PageMap pagemap_進行登記,而且在PageHeap中的Span沒有對Object進行管理。
而在CentralFreeList中的Span,它會將它管理的所有的頁都在PageMap pagemap_進行注冊,這時因為當ThreadCache歸還內存時是按Object來返回的,從Object只能找到它所在的頁,然后才能找到它所歸屬的Span,因此需要將Span管理所有的頁都進行注冊。當CentralFreeList從PageHeap申請了一個Span后,還會把Span管理的頁划分成本CentralFreeList對應的size的Objects,並且用鏈表的方式管理起來,鏈表和ThreadCache中介紹的鏈表一樣。
5. CentralFreeList內存分配
1). ThreadCache向Central Cache申請內存,Central Cache根據sizeclass選擇一個CentralFreeList;
2). CentralFreeList首先查看tc_slots_[kMaxNumTransferEntries]中是否還有未使用的空閑內存,有則直接返回給ThreadCache(即圖中的紅線1);
3). 否則從Span nonempty_中獲取空閑內存(即圖中的紅線2),如果對應的Span下的Objects分配完了,則將Span移到Spen empty_中;
4). 如果 Span nonempty_也沒有空閑內存,則從PageHeap中申請一定數量頁的內存(頁的數量由class_to_pages_[kNumClasses]確定)放到Span nonempty_中,同時會將獲取的頁在PageMap pagemap_中注冊(接口是RegisterSizeClass),並且在pagemap_cache_中注冊每頁的sizeclass(接口是CacheSizeClass()),另外還會對申請到的大塊內存划分成本CentralFreeList對應的size的Objects。然后CentralFreeList再從Span nonempty_中獲取空閑內存。

6. CentralFreeList內存釋放
1): 當ThreadCache釋放內存給Central Cache時,Central Cache根據sizeclass選擇相應的CentralFreeList;
2): 如果釋放的Object的數量正好等於映射表num_objects_to_move_[kNumClasses]中本CentralFreeList的sizeclass對應的數量, 並且tc_slots_[kMaxNumTransferEntries]還有空閑的節點, 則將釋放的Objects鏈表掛載tc_slots_[kMaxNumTransferEntries]的某個節點下(圖中藍線1)。如果沒有空閑節點了, 則將內存返給Span nonempty_。 TCMalloc在這里做了一點優化, 如果本CentralFreeList設置的tc_slots_[kMaxNumTransferEntries]中當前的slots(即CentralFreeList::cache_size_), 但還沒有達到最大值(即CentralFreeList::max_cache_size_), 則可以減少其它CentralFreeList的slots,而增加自己的slots。
3): 返回給Span nonempty_ 的Objects是一個Object一個Object返回的,因為從ThreadCache返回的所有Objects不一定是屬於同一個Span的,而Span管理的Object必須是從原來從PageHeap中申請的連續頁的內存,所以原來的Objects從哪個Span申請,必須返回到哪個Span。
4): 如果Span原來管理的所有的Objects都返回到了Span中(span->refcount == 0)[注意: 最后一個Object不需要插入鏈表,因為PageHeap關心整塊內存,不關心Object],即沒有被central cache中的tc_slots_[]緩存 或Thread cache使用,則需要將這個Span管理的內存歸還給PageHeap。
四. PageHeap
PageHeap在TCMalloc中主要作為Central Cache和操作系統之間的內存緩存和大塊內存的申請和釋放。PageHeap對內存的管理是通過Span來管理的,關於Span的描述見Central Cache部分。
在介紹PageHeap的整個結構之前,需要先介紹兩個映射表,它們分別是PageMap pagemap_和PageMapCache pagemap_cache_。
1. PageMap pagemap_
pagemap_是作為PageID(即頁ID)和它歸屬的Span之間的映射表,TCMalloc為32位系統和64為系統設計不同數據結構,32位系統使用二級的Radix Tree(TCMalloc_PageMap2),而64位系統使用三級Radix Tree(TCMalloc_PageMap3)。對不同的系統采用不同的數據結構主要是考慮這個映射表占用內存的大小。
下圖是32為系統,8K頁(kPageShift=13)的pagemap_,8K頁總共有2^19頁(32-13),將高5bits作為root[],而低14位作為Leaf[]。

在當前的__x86_64__處理器中,只用了地址的低48bits用於虛擬和物理地址的轉換,高16bits是不用的。所以在8K頁的配置下,總共有2^35頁(48-13),TCMalloc將35bits分為12,12,11三級Radix Tree,如下圖所示。

上面數據結構中的Node和Leaf只有在需要的時候才創建,因此pagemap_實際占用的內存比較少。
2. PageMapCache pagemap_cache_
pagemap_cache_是作為PageID和sizeclass的映射表,即當CentralFreeList從PageHeap獲取一頁時,就需要將這頁的PageID和CentralFreeList所屬的sizeclass在這個表中進行注冊。 pagemap_cache_實際上是個哈希數組,對於默認kHashbits = 16(即哈希key占用的比特位為16bits, 2^16 = 65536,),kValuebits = 7(即value占用7bits),8k頁來說(PageID最大值為19bits),哈希的結構如下所示,哈希的key為PageID的低16bits最為數組的下標,即哈希數組的大小為65536,對將PageID的高3bits和sizeclass(占低7bits)進行位的組合作為哈希的value。

3. PageHeap的實現
PageHeap主要作為Central Cache和操作系統之間的內存緩存和大塊內存的申請和釋放。PageHeap有兩塊緩存,一個是用於管理內存小於等於1M的連續頁內存,即SpanList free_[kMaxPages],另一個是那些內存大於1M的連續頁內存,即SpanList large_。
SpanList free_[kMaxPages]數組是按頁大小遞增的,即free_[1]是存放管理1頁的Span,free_[2]是存放管理2頁的Span,依此類推。SpanList結構下面有兩個Span鏈表,這兩個鏈表作用是不同的,Span normal存放的那些還沒有釋放給系統的Span,而Span returned則存放的是那些已經釋放給系統的Span。
{需要注意的是,TCMalloc調用內存釋放的接口TCMalloc_SystemRelease,而對應的系統調用的接口是madvise(),建議系統的行為是MADV_FREE,而MADV_FREE則將這些頁標識為延遲回收。當內核內存緊張時,這些頁將會被優先回收,如果應用程序在頁回收后又再次訪問,內核將會返回一個新的並設置為0的頁。而如果內核內存充裕時,標識為MADV_FREE的頁會仍然存在,后續的訪問會清掉延遲釋放的標志位並正常讀取原來的數據,因此應用程序不檢查頁的數據,就無法知道頁的數據是否已經被丟棄。
因為 Linux 不支持 MADV_FREE,所以使用了 MADV_DONTNEED。使用 MADV_DONTNEED調用 madvise,告訴內核這段內存今后"很可能"用不到了,其映射的物理內存盡管拿去好了,因此,TCMalloc_SystemRelease 只是告訴內核,物理內存可以回收以做它用,但虛擬空間還留着,下次訪問是時會產生缺頁中斷,而重新申請物理內存。
因此Span returned隊列中的內存還是可以重新被上層模塊申請使用的。}

4. PageHeap內存分配
1): 當CentralFreeList向PageHeap申請n頁內存時(接口是PageHeap::New(Length n)),PageHeap首先在free_[n].normal的隊列中查找,如果找到則返回,否則到free_[n].returned查找,如果找到則返回,否則在free_[n+1]中以相同的方法查找。
2): 在大於n的隊列中找到(假設在大小為m頁的隊列中找到),即將這塊內存分成兩塊,分別是n和(m-n),將含n頁的Span返回給CentralFreeList,而將含有(m-n)頁的Span插入(m-n)頁的SpanList中,插入過程中,還要檢查插入的(m-n)頁的左右相鄰頁是否也在這個SpanList中存在,如果存在,則將它們合並,合並后則需要找新的SpanList插入,重復這個過程;
3): 如果在SpanList free_[kMaxPages]中找不到合適的頁,則在SpanList large_中查找,查找過程和SpanList free_[kMaxPages]類似,即在large_.normal和large_.returned中查找最合適的Span。
4): 如果在上述的SpanList free_[kMaxPages]和SpanList large_中都找不到合適的Span,並且PageHeap中還有大量的空閑頁,說明在PageHeap中存在大量的內存碎片,則將Span進行盡可能的合並。然后再從SpanList free_[kMaxPages]或SpanList large_查找合適的頁的Span。
5): 如果上述都找不到合適的Span,則從系統申請內存來擴充PageHeap(接口: PageHeap::GrowHeap(Length n)),然后再從PageHeap獲取內存。
5. PageHeap內存釋放
1): 當CentralFreeList的某個Span所管理的內存都已經返回給這個Span后(有Span->refcount指示),CentralFreeList就將相應的Span管理的內存歸還給PageHeap(接口: PageHeap::Delete(Span* span))。
2): PageHeap會將這個Span和 free_[n].normal或larg_.normal中的Span進行合並(前提是這個Span管理的頁的前頁或后頁在相應的Span鏈表中)。如果aggressive_decommit_為TRUE(表示每次回到PageHeap的內存都要歸還給系統),則也可以和free_[n].returned或larg_.returned合並,並且都歸還給系統(系統會將物理內存收回作為他用,虛擬進程中的虛擬內存還是存在的)。
3): 查看是否需要向系統釋放內存,如果需要,則以Round Robin的方式將某個SpanList的尾部的Span釋放給系統。釋放內存是根據配置和PageHeap中累積的Page數量來執行的,具體的算法見函數PageHeap::IncrementalScavenge(Length n)。
6. 大塊內存的分配和釋放
TCMalloc對那些一次申請大於256K內存就不在經過ThreadCache,而是直接從PageHeap中分配,分配的接口是do_malloc_pages(ThreadCache* heap, size_t size),對應的PageHeap的接口是PageHeap::New(Length n),這個接口在PageHeap內存分配中已經介紹過了,這里不在累贅了,需要注意的是,通過do_malloc_pages(ThreadCache* heap, size_t size)這個接口獲取的內存,需要將第一個的PageID在 pagemap_cache_中將對應的sizeclass置為0,這樣在釋放內存的時候就知道是大內存,就可以直接調用釋放內存的接口PageHeap::Delete(Span* span)將內存直接返還給PageHeap。