TCMalloc - 基本流程

SizeMap

tcmalloc經過classid將不一樣的小對象映射到不一樣的對象桶中,sizemap記錄了一些對象大小和對象class的映射以及反向映射,除此以外,還記錄了一些ThreadCache與CentralCache層交互的時候批量處理的一些數據。
class_to_size_[kClassSizesMax]數組記錄每一個class中存儲的對象大小
class_to_pages_[kClassSizesMax]數組記錄當centraol cache向page map申請內存時一次申請的page數量
num_objects_to_move_[kClassSizesMax]數組記錄每一個線程與central cache層申請和釋放時批量處理的對象個數
 

ThreadCache

顧名思義,每一個線程一分內存Cache,線程在分配和釋放內存的時候,首先從ThreadCache中分配和釋放,沒有鎖爭用開銷,很是高效。
最重要的數據結構,就是FreeList      list_[kClassSizesMax],每一個class一個桶,每一個桶中是一個FreeList單鏈表,將全部該class的對象都連接起來。另外還有一個ThreadCache的prev和next指針,主要用來作數據統計用。
tcmalloc中全部的鏈表都是上一個對象的頭部4字節或8字節中存儲下一個對象的地址,以下所示:

 

CentralCache

CentralCache是全部線程共同使用的公共小對象池。CentralCache由一些CentralFreeList組成,每一個CentralFreeList管理一個class的小對象,線程在訪問CentralCache的時候須要加鎖,鎖的粒度是每一個對象桶CentralFreeList。
CetralFreeList的核心數據結構是兩個Span鏈表,一個叫empty_,另外一個叫nonempty_。empty_鏈表中是其全部object都已經分配完畢的span,nonempty_鏈表中是部分分配或未分配object的span。因爲ThreadCache與CentralCache每次批發的objects數量是恆定的(由上文中提到的num_objects_to_move_來肯定),而對span的操做相對來講比較耗時,因此CentralFreeList中加入了名叫tc_slots_的cache,將其做爲span的一個cache,每次分配內存和回收內存的時候,若是分配和回收的內存恰好是num_objects_to_move_中相應的數量,則優先走tc_slots_。每次分配的時候,優先從tc_slots_中尋找特定數量的objects,每次回收內存的時候,則優先將objects回收到tc_slots_中。
 

PageHeap

PageHeap是page級別的allocator,它的主要職責就是管理page,上文中提到的span,就是若干連續page組成的數據結構。
PageHeap的核心數據結構有:
1)名爲pagemap_的PageMap,它是用來映射Page地址PageID和Span的。這是一個三層的radix_tree,提供的最主要的接口有兩個,一是名爲set的設置PageID和Span的映射,另外一個是名爲get的經過PageID獲取Span。
2)名爲free_[kMaxPages]的一個SpanList數據,kMaxPages在4k頁的系統中是255,因此free_中有1個Page到255個Page的全部規格,span能夠按照任意大小進行拆分和組合。
3)名爲large_的SpanList,它用來存放大於kMaxPages的超大Page。
空閒span的夥伴系統爲上層提供span的分配與回收。當須要的span沒有空閒時,能夠把更大尺寸的span拆小(若是大的span都沒有了,則須要從新找kernel分配);當span回收時,又須要判斷相鄰的span是否空閒,以便將它們組合。判斷相鄰span仍是要用到radix tree,radix tree就像一個大數組,很容易取到當前span先後相鄰的span。
在向tcmalloc申請n個page的Span的時候,優先從free_數組的第n個下標開始尋找,若是free_[n]鏈表非空,就從這個鏈表中摘取一個元素,不然,從第n+1個下標開始尋找第一個空閒span,找到後將它切分紅n個Page和K個Page,n個Page返回給應用,K個Page插入到free_[k]鏈表中,若是依然找不到空閒Span,就向操做系統申請一大塊內存再分配。
 
每一個SpanList由兩個Span鏈表組成,一個是normal,一個是returned。normal鏈表是正常的空閒鏈表,returned鏈表是將要歸還給操做系統的Span組成的空閒鏈表。在使用MallocExtension::GetStats打印出來tcmalloc的內存佔用信息中,free_bytes統計來自normal鏈表,unmapped_bytes統計來自returned鏈表。
 
 

內存的分配過程

根據請求size判斷是大塊內存仍是小塊內存(256KB爲邊界,這個信息經過查詢SizeMap表得到)
1,小塊內存
     1), 經過size從SizeMap表中查到改請求對應的classid
     2), 從當前線程所在的ThreadCache的list_[classid]空閒鏈表中分配,若是分配成功則返回
     3), 嘗試從CentralCache.list[class]空閒鏈表中一次性批發一批空閒object,返回一個給用戶,其他的加入到ThreadCache.list_[class]空閒鏈表中。
     CentralFreeList中申請一批空閒object的申請順序是:
     1) 優先從tc_slots中獲取該數量的objects,tc_slots是一批空閒object的Cache,獲取到則返回
     2) 從nonempty_鏈表中獲取一個span,從該span中截取該數量的一批Objects,若是截取完畢以後span內的objects都用完了,那麼將這個span插入到empty_鏈表中,不然增長span的引用計數,增長的數量是object的數量,獲取到足夠數量則返回
     3) 從nonempty_鏈表中嘗試獲取足量objects
     4) 向PageHeap申請若干個Page做爲一個span,而後從span中截取足量的Objects,將截取完畢以後的span加入到nonempty_鏈表中
     PageHeap中申請n個Page的順序是:
     1) 從PageHeap的free_[n]開始尋找空閒Span,首先嚐試從free_[n].normal鏈表中尋找一個空閒Span,找到後從這個Span中截取n個Page返回,剩下的Page放入到對應的free_[x]鏈表中; 若是normal中獲取不到,那麼從free_[n].returned鏈表中尋找一個空閒的Span,而後作切分的操做。free[n]中找不到,從free[n+1]開始找,直到free[kMaxPages]
     2) 從large_鏈表中尋找,依然是先normal鏈表,再returned鏈表,找到後作切分的操做
     3) 嘗試從操做系統申請一塊至少kMaxPages大小的內存,繼續1) 2)中的步驟
2,大塊內存
     大塊內存首先計算出須要多少個Page,而後從PageHeap中申請n個Page,流程同上。
 

內存的回收流程

    1,經過ptr的地址找到它所在的Page。對於4k Page的系統來講,每一個地址除以4k就是它對應的PageId。
     2,經過PageId在pageheap中找到它對應的Span數據結構,從Span中獲取到object所屬的class。
    3,若是上面的Span中記錄的class是0,說明這是一塊大內存,直接調用PageHeap.Delete(Span*)來釋放這塊大內存;不然是一塊小內存,小內存的回收:
        3.1 將這塊內存插入到ThreadCache.freeList[cl]鏈表中,若是發現插入後的總長度大於了max_length,則嘗試將若干個objects集體回收到centralCache中。
        3.2 嘗試將這些Objects放入centralCache[cl]的FreeList中,若是發現centrailCache[cl]的FreeList已經有足夠多的元素,則將這些objects集體回收到span中(經過調用ReleaseListToSpans(start)實現)。
        3.3 經過start地址找到pageId,而後在pageheap中經過PageId找到這些objects所屬的span,將這些對象都插入到span->objects列表中。這一步中若是發現這個span上的objects都沒有使用者了,就調用PageHeap.Delete(Span*)來釋放這個Span。
 
     4, 釋放Span:
        4.1 將這個span嘗試放入到normal_freelist中。這個過程當中,會嘗試將pageheap中與此span內存地址相鄰的span作合併,以後將合併後的span插入到free_[span->length]鏈表(小於128個page)或large_鏈表(大於128個page)中
        4.2 調用PageHeap::IncrementalScavenge(span->length)嘗試去歸還一部分Span給系統,並將這個Span插入到free_[span->length]鏈表或者large_鏈表的returned隊列中。
        TCMalloc調用內存釋放的接口TCMalloc_SystemRelease,它對應的系統調用的接口是madvise(),建議系統的行爲是MADV_FREE,而MADV_FREE則將這些頁標識爲延遲迴收。當內核內存緊張時,這些頁將會被優先回收,若是應用程序在頁回收後又再次訪問,內核將會返回一個新的並設置爲0的頁。而若是內核內存充裕時,標識爲MADV_FREE的頁會仍然存在,後續的訪問會清掉延遲釋放的標誌位並正常讀取原來的數據,所以應用程序不檢查頁的數據,就沒法知道頁的數據是否已經被丟棄。
      由於 Linux 不支持 MADV_FREE,因此使用了 MADV_DONTNEED。使用 MADV_DONTNEED調用 madvise,告訴內核這段內存從此"極可能"用不到了,其映射的物理內存儘管拿去好了,所以,TCMalloc_SystemRelease 只是告訴內核,物理內存能夠回收以作它用,但虛擬空間還留着,下次訪問時會產生缺頁中斷,這個缺頁中斷會觸發從新申請物理內存的操做。
      所以Span returned隊列中的內存仍是能夠從新被上層模塊申請使用的。
相關文章
相關標籤/搜索