PooledByteBuf記憶體池-------這個我現在不太懂
阿新 • • 發佈:2018-11-08
轉載自:http://blog.csdn.net/youaremoon/article/details/47910971
http://blog.csdn.net/youaremoon/article/details/47984409
http://blog.csdn.net/youaremoon/article/details/48085591
http://blog.csdn.net/youaremoon/article/details/48184429
http://blog.csdn.net/youaremoon/article/details/50042373
http://blog.csdn.net/youaremoon/article/details/50054387
從netty 4開始,netty加入了記憶體池管理,採用記憶體池管理比普通的new ByteBuf效能提高了數十倍。
首先介紹PoolChunk, 該類主要負責記憶體塊的分配與回收,首先來看看兩個重要的術語:
page: 可以分配的最小的記憶體塊單位。
chunk: 一堆page的集合。
下面一張圖直觀的表述了PoolChunk是如何管理記憶體的:
上圖中是一個預設大小的chunk, 由2048個page組成了一個chunk,一個page的大小為8192, chunk之上有11層節點,最後一層節點數與page數量相等。每次記憶體分配需要保證記憶體的連續性,這樣才能簡單的操作分配到的記憶體,因此這裡構造了一顆完整的平衡二叉樹,所有子節點的管理的記憶體也屬於其父節點。如果想獲取一個8K的記憶體,則只需在第11層找一個可用節點即可,而如果需要16K的資料,則需要在第10層找一個可用節點,因為需要兩個第11層節點。如果一個節點存在一個已經被分配的子節點,則該節點不能被分配,例如需要16K記憶體,這個時候id=2048的節點已經被分配,id=2049的節點未分配,就不能直接分配1024這個節點,因為這個節點下的記憶體只有8K了。
通過上面這個樹結構,可以看到每次記憶體分配都是8K*(2^n), 比如需要24K記憶體時,實際上會申請到一塊32K的記憶體。為了分配一個大小為chunkSize/(2^i)的記憶體段,需要在深度為i的層從左開始查詢可用節點。如想分配16K的記憶體,chunkSize = 16M( 2048個page * 8K ), 則i=10, 需要從第10層找一個空閒的節點分配記憶體。
負責記憶體分配的PoolChunk類,它最小的分配單位為page, 而預設的page size為8K。在實際的應用中,會存在很多小塊記憶體的分配,如果小塊記憶體也佔用一個page明顯很浪費,針對這種情況,可以將8K的page拆成更小的塊,這已經超出chunk的管理範圍了,這個時候就出現了PoolSubpage, 其實PoolSubpage做的事情和PoolChunk做的事情類似,只是PoolSubpage管理的是更小的一段記憶體。
如上圖,PoolSubpage將chunk中的一個page再次劃分,分成相同大小的N份,這裡暫且叫Element,通過對每一個Element的標記與清理標記來進行記憶體的分配與釋放。
介紹了PoolChunk以及針對page的更細粒度的PoolSubpage,其實在chunk的上層還有一個管理類:PoolChunkList,PoolChunkList負責管理多個chunk的生命週期,在此基礎上對記憶體分配進行進一步的優化。
PoolChunkList主要是為了提高記憶體分配的效率,每個list中包含多個chunk,而多個list又可以形成一個大的link list,在進行記憶體分配時,先從比較靠前的list中分配記憶體,這樣分配到的機率更大。在高峰期申請過多的記憶體後,隨著流量下降慢慢的釋放掉多餘記憶體,形成一個良性的迴圈。下圖是上述三個類的層次結構:
已經講到了記憶體池中的幾個重要的類:
1、PoolChunk:維護一段連續記憶體,並負責記憶體塊分配與回收,其中比較重要的兩個概念:page:可分配的最小記憶體塊單位;chunk:page的集合;
2、PoolSubpage:將page分為更小的塊進行維護;
3、PoolChunkList:維護多個PoolChunk的生命週期。
多個PoolChunkList也會形成一個list,方便記憶體的管理。最終由PoolArena對這一系列類進行管理,PoolArena本身是一個抽象類,其子類為HeapArena和DirectArena,對應堆記憶體(heap buffer)和堆外記憶體(direct buffer),除了操作的記憶體(byte[]和ByteBuffer)不同外兩個類完全一致。
記憶體池記憶體分配流程:
1、ByteBufAllocator 準備申請一塊記憶體;
2、嘗試從PoolThreadCache中獲取可用記憶體,如果成功則完成此次分配,否則繼續往下走,注意後面的記憶體分配都會加鎖;
3、如果是小塊(可配置該值)記憶體分配,則嘗試從PoolArena中快取的PoolSubpage中獲取記憶體,如果成功則完成此次分配;
4、如果是普通大小的記憶體分配,則從PoolChunkList中查詢可用PoolChunk並進行記憶體分配,如果沒有可用的PoolChunk則建立一個並加入到PoolChunkList中,完成此次記憶體分配;
5、如果是大塊(大於一個chunk的大小)記憶體分配,則直接分配記憶體而不用記憶體池的方式;
6、記憶體使用完成後進行釋放,釋放的時候首先判斷是否和分配的時候是同一個執行緒,如果是則嘗試將其放入PoolThreadCache,這塊記憶體將會在下一次同一個執行緒申請記憶體時使用,即前面的步驟2;
7、如果不是同一個執行緒,則回收至chunk中,此時chunk中的記憶體使用率會發生變化,可能導致該chunk在不同的PoolChunkList中移動,或者整個chunk回收(chunk在q000上,且其分配的所有記憶體被釋放);同時如果釋放的是小塊記憶體(與步驟3中描述的記憶體相同),會嘗試將小塊記憶體前置到PoolArena中,這裡操作成功了,步驟3的操作中才可能成功。
在PoolThreadCache中分了tinySubPageHeapCaches、smallSubPageHeapCaches、normalSubPageHeapCaches三個陣列,對應於tiny\small\normal在記憶體分配上的不同(tiny和small使用subpage,normal使用page)。
到此,netty記憶體池相關介紹已經完,netty就是實現了兩個比較經典的分配策略,buddy allocation(見PoolChunk)和jemalloc(有一定改動,PooledByteBufAllocator+PoolArena+PoolChunk+PoolThreadCache),所以如果想了解更新的資訊,可以按照上面兩個關鍵詞搜尋,或則看轉載的原文。
netty記憶體池可調優引數
引數名 | 說明 | 預設值 |
io.netty.allocator.pageSize | page的大小 | 8192 |
io.netty.allocator.maxOrder | 一個chunk的大小=pageSize << maxOrder | 11 |
io.netty.allocator.numHeapArenas | heap arena的個數 | min(cpu核數,maxMemory/chunkSize/6),一般來說會=cpu核數 |
io.netty.allocator.numDirectArenas | direct arena的個數 | min(cpu核數,directMemory/chunkSize/6),一般來說會=cpu核數 |
io.netty.allocator.tinyCacheSize | PoolThreadCache中tiny cache每個MemoryRegionCache中的Entry個數 | 512 |
io.netty.allocator.smallCacheSize | PoolThreadCache中small cache每個MemoryRegionCache中的Entry個數 | 256 |
io.netty.allocator.normalCacheSize | PoolThreadCache中normal cache每個MemoryRegionCache中的Entry個數 | 64 |
io.netty.allocator.maxCachedBufferCapacity | PoolThreadCache中normal cache陣列長度 | 32 * 1024 |
io.netty.allocator.cacheTrimInterval | PoolThreadCache中的cache收縮閾值,每隔該值次數,會進行一次收縮 | 8192 |
io.netty.allocator.type | allocator型別,如果不使用記憶體池,則設定為unpooled | pooled |
io.netty.noUnsafe | 是否關閉direct buffer | false |
io.netty.leakDetectionLevel | 記憶體洩露檢測級別 | SIMPLE |