Netty14# 池化內存之線程緩存

前言

在前面文章『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, 00000); // 註解@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 > queue封裝的均爲ByteBuffer。下面看下不同類型緩存的ByteBuffer是如何分佈的。

 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 > queue中的T即ByteBuffer。

緩存歸隊

再回到添加方法中,上面通過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源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章