1. JEMalloc分配算法
Netty的PooledByteBuf
采用與jemalloc一致的內存分配算法。可用這樣的情景類比,想像一下當前電商的配送流程。當顧客采購小件商品(比如書籍)時,直接從同城倉庫送出;當顧客采購大件商品(比如電視)時,從區域倉庫送出;當顧客采購超大件商品(比如汽車)時,則從全國倉庫送出。Netty的分配算法與此相似,可參見下圖:
稍有不同的是:在Netty中,小件商品和大件商品都首先從同城倉庫(ThreadCache-tcache)送出;如果同城倉庫沒有,則會從區域倉庫(Arena)送出。
對于商品分類,Netty根據每次請求分配內存的大小,將請求分為如下幾類:
注意以下幾點:
- 內存分配的最小單位為16B。
- < 512B的請求為Tiny,< 8KB(PageSize)的請求為Small,<= 16MB(ChunkSize)的請求為Normal,> 16MB(ChunkSize)的請求為Huge。
- < 512B的請求以16B為起點每次增加16B;>= 512B的請求則每次加倍。
- 不在表格中的請求大小,將向上規范化到表格中的數據,比如:請求分配511B、512B、513B,將依次規范化為512B、512B、1KB。
1.1 Arena
為了提高內存分配效率并減少內部碎片,jemalloc算法將Arena切分為小塊Chunk,根據每塊的內存使用率又將小塊組合為以下幾種狀態:QINIT,Q0,Q25,Q50,Q75,Q100。Chunk塊可以在這幾種狀態間隨著內存使用率的變化進行轉移,內存使用率和狀態轉移可參見下圖:
其中橫軸表示內存使用率(百分比),縱軸表示狀態,可知:
- QINIT的內存使用率為[0,25)、Q0為(0,50)、Q100為[100,100]。
- Chunk塊的初始狀態為QINIT,當使用率達到25時轉移到Q0狀態,再次達到50時轉移到Q25,依次類推直到Q100;當內存釋放時又從Q100轉移到Q75,直到Q0狀態且內存使用率為0時,該Chunk從Arena中刪除。注意極端情況下,Chunk可能從QINIT轉移到Q0再釋放全部內存,然后從Arena中刪除。
1.2 Chunk和Page
雖然已將Arena切分為小塊Chunk,但實際上Chunk是相當大的內存塊,在jemalloc中建議為4MB,Netty默認使用16MB。為了進一步提高內存利用率并減少內部碎片,需要繼續將Chunk切分為小的塊Page。一個典型的切分將Chunk切分為2048塊,Netty正是如此,可知Page的大小為:16MB/2048=8KB。一個好的內存分配算法,應使得已分配內存塊盡可能保持連續,這將大大減少內部碎片,由此jemalloc使用伙伴分配算法盡可能提高連續性。伙伴分配算法的示意圖如下:
圖中最底層表示一個被切分為2048個Page的Chunk塊。自底向上,每一層節點作為上一層的子節點構造出一棵滿二叉樹,然后按層分配滿足要求的內存塊。以待分配序列8KB、16KB、8KB為例分析分配過程(每個Page大小8KB):
- 8KB--需要一個Page,第11層滿足要求,故分配2048節點即Page0;
- 16KB--需要兩個Page,故需要在第10層進行分配,而1024的子節點2048已分配,從左到右找到滿足要求的1025節點,故分配節點1025即Page2和Page3;
- 8KB--需要一個Page,第11層滿足要求,2048已分配,從左到右找到2049節點即Page1進行分配。
分配結束后,已分配連續的Page0-Page3,這樣的連續內存塊,大大減少內部碎片并提高內存使用率。
1.3 SubPage
Netty中每個Page的默認大小為8KB,在實際使用中,很多業務需要分配更小的內存塊比如16B、32B、64B等。為了應對這種需求,需要進一步切分Page成更小的SubPage。SubPage是jemalloc中內存分配的最小單位,不能再進行切分。SubPage切分的單位并不固定,以第一次請求分配的大小為單位(最小切分單位為16B)。比如,第一次請求分配32B,則Page按照32B均等切分為256塊;第一次請求16B,則Page按照16B均等切分為512塊。為了便于內存分配和管理,根據SubPage的切分單位進行分組,每組使用雙向鏈表組合,示意圖如下:
其中每組的頭結點head只用來標記該組的大小,之后的節點才是實際分配的SubPage節點。需要注意的是,這些節點正是上一節中滿二叉樹的葉子節點即一個Page。
至此,已介紹完jemalloc的基本思想,關于具體實現細節,可跳轉: