前言
在前面文章『Netty12# 池化內存框架流程』Netty會將不同的內存尺寸緩存起來,每個線程綁定了專屬邏輯內存區域(PoolArena),減少資源競爭。每個線程綁定了緩存PoolThreadCache,內存分配時,先從當前線程綁定的PoolThreadCache緩存分配。
下圖爲涉及到相關類的關係圖:
工作過程:
@1 通過引導類傳入NioEventLoopGroup,線程工廠創建的線程均爲FastThreadLocalThread
@2 FastThreadLocalThread持有InternalThreadLocalMap(內部維護一個對象數組)
@3 當通過PooledByteBufAllocator#newDirectBuffer分配內存時,通過調用PoolThreadLocalCache#get()完成對InternalThreadLocalMap的第一次填充,對象數組下標爲線程索引號,其對應的值爲PoolThreadCache。
@4 PoolThreadCache是被當前線程緩存的對象
PoolThreadLocalCache繼承了線程類FastThreadLocal,FastThreadLocal的作用類似ThreadLocal,傳遞線程上下文變量。本小節梳理PoolThreadLocalCache工作流程。
構造函數
final class PoolThreadLocalCache extends FastThreadLocal<PoolThreadCache> {
private final boolean useCacheForAllThreads;
PoolThreadLocalCache(boolean useCacheForAllThreads) {
this.useCacheForAllThreads = useCacheForAllThreads;
}
// ...
}
小結:構造函數就一個變量useCacheForAllThreads,默認true,使用線程緩存,可以通過-Dio.netty.allocator.useCacheForAllThread制定。
初始化方法賦值
@Override
protected synchronized PoolThreadCache initialValue() {
final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas);
final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas); // 註解@1
final Thread current = Thread.currentThread();
if (useCacheForAllThreads || current instanceof FastThreadLocalThread) { // 註解@2
final PoolThreadCache cache = new PoolThreadCache(
heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize,
DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL);
if (DEFAULT_CACHE_TRIM_INTERVAL_MILLIS > 0) {
final EventExecutor executor = ThreadExecutorMap.currentExecutor();
if (executor != null) {
executor.scheduleAtFixedRate(trimTask, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS,
DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
}
}
return cache;
}
// No caching so just use 0 as sizes.
return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0); // 註解@3
}
註解@1:heapArenas/directArenas:Arena數組,元素爲HeapArena/DirectArena。調用了同一個方法leastUsedArena()。
private <T> PoolArena<T> leastUsedArena(PoolArena<T>[] arenas) {
if (arenas == null || arenas.length == 0) {
return null;
}
PoolArena<T> minArena = arenas[0];
for (int i = 1; i < arenas.length; i++) {
PoolArena<T> arena = arenas[i];
if (arena.numThreadCaches.get() < minArena.numThreadCaches.get()) {
minArena = arena;
}
}
return minArena;
}
每個線程都會綁定PoolArena,在leastUsedArena()輪詢一遍,獲取當前綁定線程數最少的PoolArena。
註解@2:當useCacheForAllThreads=true(默認true)和當前thread屬於FastThreadLocalThread才構造PoolThreadCache進行緩存。
DEFAULT_CACHE_TRIM_INTERVAL_MILLIS:定時釋放緩存。默認爲0表示關閉,可以通過-Dio.netty.allocator.cacheTrimIntervalMillis指定。
private final Runnable trimTask = new Runnable() {
@Override
public void run() {
PooledByteBufAllocator.this.trimCurrentThreadCache();
}
};
public boolean trimCurrentThreadCache() {
PoolThreadCache cache = threadCache.getIfExists();
if (cache != null) {
cache.trim();
return true;
}
return false;
}
通過定時調度調用PoolThreadCache的trim()方法將線程緩存釋放。
註解@3:禁用線程緩存依然是構造PoolThreadCache,只是傳入的參數爲0.
小結:初始化賦值過程實際是爲了創建一個PoolThreadCache對象。
初始化方法調用
初始化方法PoolThreadLocalCache#initialValue()什麼時候調用的呢?在第一次調用FastThreadLocal#get()時進行的初始化。例如:在PooledByteBufAllocator#newDirectBuffer()方法中PoolThreadCache cache = threadCache.get();
public final V get() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap);
}
初始化後,會將放入InternalThreadLocalMap, 其中維護了一個對象數組Object[],下標即爲index,每創建一個線程FastThreadLocal,都會遞增一個index。
private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = null;
try {
v = initialValue();
} catch (Exception e) {
PlatformDependent.throwException(e);
}
// 放入InternalThreadLocalMap中實際爲數組
threadLocalMap.setIndexedVariable(index, v);
addToVariablesToRemove(threadLocalMap, this);
return v;
}
private final int index;
public FastThreadLocal() {
// 每創建一個fast線程都會分配一個index
index = InternalThreadLocalMap.nextVariableIndex();
}
小結:初始化方法initialValue(),在第一次調用threadCache.get()的時候執行。並將初始化的結果PoolThreadCache放入InternalThreadLocalMap(實際爲對象數組)。
FastThreadLocalThread的調用
在初始化賦值註解@2中,只有滿足兩個條件纔會緩存,if (useCacheForAllThreads || current instanceof FastThreadLocalThread) 。其中一個是當前線程屬於FastThreadLocalThread。那問題是我們有用FastThreadLocalThread嗎?
在通過引導類構建Netty客戶端和服務端時會傳入EventLoopGroup,我們以NioEventLoopGroup看下它創建的是什麼線程。
EventLoopGroup group = new NioEventLoopGroup();
通過NioEventLoopGroup的構造函數可以跟到下面內容:
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
// ...
}
通過newDefaultThreadFactory()看下線程工廠類DefaultThreadFactory中如何創建線程的。
@Override
public Thread newThread(Runnable r) {
Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet());
try {
if (t.isDaemon() != daemon) {
t.setDaemon(daemon);
}
if (t.getPriority() != priority) {
t.setPriority(priority);
}
} catch (Exception ignored) {
// Doesn't matter even if failed to set.
}
return t;
}
protected Thread newThread(Runnable r, String name) {
return new FastThreadLocalThread(threadGroup, r, name); // 實際爲FastThreadLocalThread實例。
}
通過newThread創建的實際爲FastThreadLocalThread實例。
小結:我們通過Bootstrap引導類傳入的NioEventLoopGroup,使用的線程爲FastThreadLocalThread。
PoolThreadCache 緩存了三個級別的緩存類型,分別爲tiny、small、normal。
構造函數
PoolThreadCache(PoolArena<byte[]> heapArena, PoolArena<ByteBuffer> directArena,
int tinyCacheSize, int smallCacheSize, int normalCacheSize,
int maxCachedBufferCapacity, int freeSweepAllocationThreshold) {
if (directArena != null) {
tinySubPageDirectCaches = createSubPageCaches(
tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny);
smallSubPageDirectCaches = createSubPageCaches(
smallCacheSize, directArena.numSmallSubpagePools, SizeClass.Small);
numShiftsNormalDirect = log2(directArena.pageSize);
normalDirectCaches = createNormalCaches(
normalCacheSize, maxCachedBufferCapacity, directArena);
directArena.numThreadCaches.getAndIncrement();
}
// ...
}
參數說明
heapArena:最少持有線程數(使用率最少)的邏輯堆內存PoolArena,PoolArena[]數組長度默認爲核數的2倍
directArena:最少持有線程數(使用率最少)的邏輯堆外直接內存PoolArena,PoolArena[]數組長度默認爲核數的2倍
tinyCacheSize:默認tiny類型緩存池大小512
smallCacheSize:默認small類型緩存池大小爲256
normalCacheSize:默認normal類型緩存池大小爲64
maxCachedBufferCapacity:默認爲32KB,用於限制normal緩存數組的長度
freeSweepAllocationThreshold:默認8192,分配次數閾值,超過後釋放內存池
構造函數中,主要給三種類型的緩存數組賦值,包括堆內存和堆外直接內存,結構一致,只走查堆外直接內存。
// tiny類型緩存數組
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
// small類型緩存數組
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
// normal類型緩存數組
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;
createSubPageCaches
tiny類型緩存數組與small類型緩存數組調用調用相同的createSubPageCaches()方法。
private static <T> MemoryRegionCache<T>[] createSubPageCaches(
int cacheSize, int numCaches, SizeClass sizeClass) {
if (cacheSize > 0 && numCaches > 0) {
@SuppressWarnings("unchecked")
MemoryRegionCache<T>[] cache = new MemoryRegionCache[numCaches];
for (int i = 0; i < cache.length; i++) {
cache[i] = new SubPageMemoryRegionCache<T>(cacheSize, sizeClass);
}
return cache;
} else {
return null;
}
}
方法入參
cacheSize:MemoryRegionCache包含隊列Queue的大小,tiny類型512,small類型256
numCaches:不同緩存類型的規格數量。
tiny類型規格數量爲32,計算方式 PoolArena.numTinySubpagePools=512 >>> 4=32
small類型規格數量爲4,計算方式 heapArena.numSmallSubpagePools=pageShifts - 9=13 - 9 = 4
小結:tiny類型會構建MemoryRegionCache的數組長度爲32,每個數組元素爲SubPageMemoryRegionCache(包含Queue的大小爲512);
small類型會構建MemoryRegionCache的數組長度爲4,每個數組元素爲SubPageMemoryRegionCache(包含Queue的大小爲256)
createNormalCaches
Normal類型緩存數組調用createNormalCaches()方法。
private static <T> MemoryRegionCache<T>[] createNormalCaches(
int cacheSize, int maxCachedBufferCapacity, PoolArena<T> area) {
if (cacheSize > 0 && maxCachedBufferCapacity > 0) {
int max = Math.min(area.chunkSize, maxCachedBufferCapacity);
int arraySize = Math.max(1, log2(max / area.pageSize) + 1);
@SuppressWarnings("unchecked")
MemoryRegionCache<T>[] cache = new MemoryRegionCache[arraySize];
for (int i = 0; i < cache.length; i++) {
cache[i] = new NormalMemoryRegionCache<T>(cacheSize);
}
return cache;
} else {
return null;
}
}
方法入參
cacheSize:Normal類型64
maxCachedBufferCapacity:32K
數組大小計算
int arraySize = Math.max(1, log2(max / area.pageSize) + 1);
int max:maxCachedBufferCapacity=32KB;area.chunkSize = 16M,Max.min(32KB,16M) = 32K
pageSize:area.pageSize=8K
log2(max / area.pageSize),代入log2(4)公式
private static int log2(int val) {
int res = 0;
while (val > 1) {
val >>= 1;
res++;
}
return res;
}
經過計算數組大小arraySize= 3
小結:Normal類型會構建MemoryRegionCache的數組長度爲3,每個數組元素爲SubPageMemoryRegionCache(包含Queue的大小爲64)。
緩存數組結構
上面tiny、small、normal無論哪種類型都在構建MemoryRegionCache數組,通過看下MemoryRegionCache的結構看下緩存的不同點。
private abstract static class MemoryRegionCache<T> {
private final int size;
private final Queue<Entry<T>> queue;
private final SizeClass sizeClass;
private int allocations;
MemoryRegionCache(int size, SizeClass sizeClass) {
this.size = MathUtil.safeFindNextPositivePowerOfTwo(size);
queue = PlatformDependent.newFixedMpscQueue(this.size);
this.sizeClass = sizeClass;
}
// ...
}
以對外直接內存Queue<Entry
boolean add(PoolArena<?> area, PoolChunk chunk, ByteBuffer nioBuffer,
long handle, int normCapacity, SizeClass sizeClass) {
MemoryRegionCache<?> cache = cache(area, normCapacity, sizeClass);
if (cache == null) {
return false;
}
return cache.add(chunk, nioBuffer, handle);
}
通過cache()方法來判斷緩存的三種類型判斷
private MemoryRegionCache<?> cache(PoolArena<?> area, int normCapacity, SizeClass sizeClass) {
switch (sizeClass) {
case Normal:
return cacheForNormal(area, normCapacity);
case Small:
return cacheForSmall(area, normCapacity);
case Tiny:
return cacheForTiny(area, normCapacity);
default:
throw new Error();
}
}
下面逐個看看每個裏面的結構,先看Tiny類型。
private MemoryRegionCache<?> cacheForTiny(PoolArena<?> area, int normCapacity) {
// idx = normCapacity 除以 16
int idx = PoolArena.tinyIdx(normCapacity);
if (area.isDirect()) {
// tiny有32個規格類型即32個MemoryRegionCache實例
// 例如:normCapacity=32 則返回第2個數組元素MemoryRegionCache
return cache(tinySubPageDirectCaches, idx);
}
return cache(tinySubPageHeapCaches, idx);
}
static int tinyIdx(int normCapacity) {
return normCapacity >>> 4; // 相當於直接將normCapacity除以16
}
過程:Tiny類型中根據需要分配的大小除以16 示例1:normCapacity=0,idx=0,返回 tinySubPageDirectCaches[0],也就是 tinySubPageDirectCaches[0]沒有緩存。
示例2:normCapacity=16,idx=1,返回 tinySubPageDirectCaches[1],也就是 tinySubPageDirectCaches[1]中的Queue的buffer大小均爲16字節。
示例3:normCapacity=32,idx=2,返回 tinySubPageDirectCaches[2],也就是 tinySubPageDirectCaches[2]中的Queue的buffer大小均爲32字節。
...
示例4: normCapacity=496,idx=31,返回 tinySubPageDirectCaches[31],也就是 tinySubPageDirectCaches[31]中的Queue的buffer大小均爲496字節。
接着看Small類型的存儲格式
private MemoryRegionCache<?> cacheForSmall(PoolArena<?> area, int normCapacity) {
int idx = PoolArena.smallIdx(normCapacity);
if (area.isDirect()) {
return cache(smallSubPageDirectCaches, idx);
}
return cache(smallSubPageHeapCaches, idx);
}
static int smallIdx(int normCapacity) {
int tableIdx = 0;
int i = normCapacity >>> 10;
while (i != 0) {
i >>>= 1;
tableIdx ++;
}
return tableIdx;
}
過程:Small類型的分配normCapacity >>> 10,代入計算看看System.out.println(smallIdx(normCapacity))。
示例1:normCapacity=512,idx = 0,返回smallSubPageDirectCaches[0],也就是smallSubPageDirectCaches[0]中Queue的Buffer大小均爲512字節。
示例2:normCapacity=1024,idx = 1,返回smallSubPageDirectCaches[1],也就是smallSubPageDirectCaches[1]中Queue的Buffer大小均爲1024字節。
示例3:normCapacity=2048,idx = 2,返回smallSubPageDirectCaches[2],也就是smallSubPageDirectCaches[2]中Queue的Buffer大小均爲2048字節。
示例3:normCapacity=4096,idx = 3,返回smallSubPageDirectCaches[3],也就是smallSubPageDirectCaches[3]中Queue的Buffer大小均爲4096字節。
最後看下Normal類型
private MemoryRegionCache<?> cacheForNormal(PoolArena<?> area, int normCapacity) {
if (area.isDirect()) {
int idx = log2(normCapacity >> numShiftsNormalDirect);
return cache(normalDirectCaches, idx);
}
int idx = log2(normCapacity >> numShiftsNormalHeap);
return cache(normalHeapCaches, idx);
}
過程:先把numShiftsNormalDirect算下
numShiftsNormalDirect = log2(directArena.pageSize) = log2(8192) = 13.
代入公式計算下 int idx = log2(normCapacity >> 13)
示例1:normCapacity=8192(8K),idx = 0,返回normalDirectCaches[0],也就是normalDirectCaches[0]中Queue的Buffer大小均爲8KB。
示例2:normCapacity=16384(16K),idx = 1,返回normalDirectCaches[1],也就是normalDirectCaches[0]中Queue的Buffer大小均爲16KB。
示例3:normCapacity=32768(32K),idx = 2,返回normalDirectCaches[2],也就是normalDirectCaches[0]中Queue的Buffer大小均爲32KB。
小結:通過上面的過程分析,能夠得出MemoryRegionCache的緩存結構如下,其中每個數組元素的隊列中緩存的大小都是相同的,也就是Queue<Entry
緩存歸隊
再回到添加方法中,上面通過cache()方法分析了緩存數組結構,返回不同類型的MemoryRegionCache。
boolean add(PoolArena<?> area, PoolChunk chunk, ByteBuffer nioBuffer,
long handle, int normCapacity, SizeClass sizeClass) {
MemoryRegionCache<?> cache = cache(area, normCapacity, sizeClass);
if (cache == null) {
return false;
}
return cache.add(chunk, nioBuffer, handle); // 註解@1
}
註解@1:下面是將chunk(真正一塊連續內存), nioBuffer, handle(指向內存的指針)放入隊列的過程。
public final boolean add(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle) {
Entry<T> entry = newEntry(chunk, nioBuffer, handle); // 註解@2
boolean queued = queue.offer(entry); // 註解@3
if (!queued) {
// If it was not possible to cache the chunk, immediately recycle the entry
entry.recycle();
}
return queued;
}
註解@2:構造Entry對象
註解@3:將Entry放入所在規格的隊列Queue中。
小結:還有allocate()方法留在下節梳理,就內存數組結構簡單做個小結:
@1 Netty以chunk爲單位(16M)向系統申請物理內存,Netty池化內存分成了4種內存類型。Tiny(0~512Byte),Small(512Byte~8KB),Normal(8KB~16MB),Huge(>16M)
@2 Netty對Tiny、Small、Normal做了緩存,針對不同的類型通過”數組+隊列“繼續切成不同的尺寸,每個尺寸內的緩存ByteBuffer大小相同,不同尺寸之間緩存的Buffer大小以2的N次增長。
@3 Tiny類型從0到496被劃分爲32個尺寸(數組)
@4 Small類型從512到4096(4K)被劃分4個尺寸
@5 Normal類型從8192(8K)到32768(32K)被劃分爲3個尺寸
@6 在內存分配時,先根據需要分配的內存大小判斷屬於那種內存類型;進而計算出屬於該內存類型的哪個尺寸。
@7 每個尺寸都維護有隊列Queue,定位到尺寸規格也就拿到Queue中的實際緩存(PoolChunk)和指針(handle)並完成所需分配內存buffer的初始化。
本文分享自微信公衆號 - 瓜農老梁(gh_01130ae30a83)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。