Netty源码分析第6章: ByteBuf

第六节: 命中缓存的分配

上一小节简单分析了directArena内存分配大概流程, 知道其先命中缓存, 如果命中不到, 则区分配一款连续内存, 这一小节带大家剖析命中缓存的相关逻辑

分析先关逻辑之前, 首先介绍缓存对象的数据结构

回顾上一小节的内容, 我们讲到PoolThreadCache中维护了三个缓存数组(实际上是六个, 这里仅仅以Direct为例, heap类型的逻辑是一样的): tinySubPageDirectCaches, smallSubPageDirectCaches, 和normalDirectCaches分别代表tiny类型, small类型和normal类型的缓存数组

这三个数组保存在PoolThreadCache的成员变量中:

private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;

其中是在构造方法中进行了初始化:

tinySubPageDirectCaches = createSubPageCaches(
tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny);
smallSubPageDirectCaches = createSubPageCaches(
smallCacheSize, directArena.numSmallSubpagePools, SizeClass.Small);
normalDirectCaches = createNormalCaches(
normalCacheSize, maxCachedBufferCapacity, directArena);

我们以tiny类型为例跟到createSubPageCaches方法中:

private static <T> MemoryRegionCache<T>[] createSubPageCaches(
int cacheSize, int numCaches, SizeClass sizeClass) {
if (cacheSize > 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;
}
}

这里上面的小节已经分析过, 这里创建了一个缓存数组, 这个缓存数组的长度,也就是numCaches, 在不同的类型, 这个长度不一样, tiny类型长度是32, small类型长度为4, normal类型长度为3

我们知道, 缓存数组中每个节点代表一个缓存对象, 里面维护了一个队列, 队列大小由PooledByteBufAllocator类中的tinyCacheSize, smallCacheSize, normalCacheSize属性决定的, 这里之前小节已经剖析过

其中每个缓存对象, 队列中缓存的ByteBuf大小是固定的, netty将每种缓冲区类型分成了不同长度规格, 而每个缓存中的队列缓存的ByteBuf的长度, 都是同一个规格的长度, 而缓冲区数组的长度, 就是规格的数量

比如, 在tiny类型中, netty将其长度分成32个规格, 每个规格都是16的整数倍, 也就是包含0B, 16B, 32B, 48B, 64B, 80B, 96B......496B总共32种规格, 而在其缓存数组tinySubPageDirectCaches中, 这每一种规格代表数组中的一个缓存对象缓存的ByteBuf的大小, 我们以tinySubPageDirectCaches[1]为例(这里下标选择1是因为下标为0代表的规格是0B, 其实就代表一个空的缓存, 这里不进行举例), 在tinySubPageDirectCaches[1]的缓存对象中所缓存的ByteBuf的缓冲区长度是16B, 在tinySubPageDirectCaches[2]中缓存的ByteBuf长度都为32B, 以此类推, tinySubPageDirectCaches[31]中缓存的ByteBuf长度为496B

有关类型规则的分配如下:

tiny:总共32个规格, 均是16的整数倍, 0B, 16B, 32B, 48B, 64B, 80B, 96B......496B

small:4种规格, 512b, 1k, 2k, 4k

nomal:3种规格, 8k, 16k, 32k

这样, PoolThreadCache中缓存数组的数据结构为

5-6-1

大概了解缓存数组的数据结构, 我们再继续剖析在缓冲中分配内存的逻辑

回到PoolArena的allocate方法中:

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
//规格化
final int normCapacity = normalizeCapacity(reqCapacity);
if (isTinyOrSmall(normCapacity)) {
int tableIdx;
PoolSubpage<T>[] table;
//判断是不是tinty
boolean tiny = isTiny(normCapacity);
if (tiny) { // < 512
//缓存分配
if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
return;
}
//通过tinyIdx拿到tableIdx
tableIdx = tinyIdx(normCapacity);
//subpage的数组
table = tinySubpagePools;
} else {
if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
return;
}
tableIdx = smallIdx(normCapacity);
table = smallSubpagePools;
} //拿到对应的节点
final PoolSubpage<T> head = table[tableIdx]; synchronized (head) {
final PoolSubpage<T> s = head.next;
//默认情况下, head的next也是自身
if (s != head) {
assert s.doNotDestroy && s.elemSize == normCapacity;
long handle = s.allocate();
assert handle >= 0;
s.chunk.initBufWithSubpage(buf, handle, reqCapacity); if (tiny) {
allocationsTiny.increment();
} else {
allocationsSmall.increment();
}
return;
}
}
allocateNormal(buf, reqCapacity, normCapacity);
return;
}
if (normCapacity <= chunkSize) {
//首先在缓存上进行内存分配
if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
//分配成功, 返回
return;
}
//分配不成功, 做实际的内存分配
allocateNormal(buf, reqCapacity, normCapacity);
} else {
//大于这个值, 就不在缓存上分配
allocateHuge(buf, reqCapacity);
}
}

首先通过normalizeCapacity方法进行内存规格化

我们跟到normalizeCapacity方法中:

int normalizeCapacity(int reqCapacity) {
if (reqCapacity < 0) {
throw new IllegalArgumentException("capacity: " + reqCapacity + " (expected: 0+)");
}
if (reqCapacity >= chunkSize) {
return reqCapacity;
}
//如果>tiny
if (!isTiny(reqCapacity)) { // >= 512
//找一个2的幂次方的数值, 确保数值大于等于reqCapacity
int normalizedCapacity = reqCapacity;
normalizedCapacity --;
normalizedCapacity |= normalizedCapacity >>> 1;
normalizedCapacity |= normalizedCapacity >>> 2;
normalizedCapacity |= normalizedCapacity >>> 4;
normalizedCapacity |= normalizedCapacity >>> 8;
normalizedCapacity |= normalizedCapacity >>> 16;
normalizedCapacity ++; if (normalizedCapacity < 0) {
normalizedCapacity >>>= 1;
} return normalizedCapacity;
}
//如果是16的倍数
if ((reqCapacity & 15) == 0) {
return reqCapacity;
}
//不是16的倍数, 变成最大小于当前值的值+16
return (reqCapacity & ~15) + 16;
}

if (!isTiny(reqCapacity)) 代表如果大于tiny类型的大小, 也就是512, 则会找一个2的幂次方的数值, 确保这个数值大于等于reqCapacity

如果是tiny, 则继续往下

if ((reqCapacity & 15) == 0) 这里判断如果是16的倍数, 则直接返回

如果不是16的倍数, 则返回 (reqCapacity & ~15) + 16 , 也就是变成最小大于当前值的16的倍数值

从上面规格化逻辑看出, 这里将缓存大小规格化成固定大小, 确保每个缓存对象缓存的ByteBuf容量统一

回到allocate方法中:

if(isTinyOrSmall(normCapacity)) 这里是根据规格化后的大小判断是否tiny或者small类型, 我们跟到方法中:

boolean isTinyOrSmall(int normCapacity) {
return (normCapacity & subpageOverflowMask) == 0;
}

这里是判断如果normCapacity小于一个page的大小, 也就是8k代表其实tiny或者small

继续看allocate方法:

如果当前大小是tiny或者small, 则isTiny(normCapacity)判断是否是tiny类型, 跟进去:

static boolean isTiny(int normCapacity) {
return (normCapacity & 0xFFFFFE00) == 0;
}

这里是判断如果小于512, 则认为是tiny

再继续看allocate方法:

如果是tiny, 则通过cache.allocateTiny(this, buf, reqCapacity, normCapacity)在缓存上进行分配

我们就以tiny类型为例, 分析在缓存上分配ByteBuf的流程

allocateTiny是缓存分配的入口

我们跟进去, 进入到了PoolThreadCache的allocateTiny方法中:

boolean allocateTiny(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) {
return allocate(cacheForTiny(area, normCapacity), buf, reqCapacity);
}

这里有个方法cacheForTiny(area, normCapacity), 这个方法的作用是根据normCapacity找到tiny类型缓存数组中的一个缓存对象

我们跟进cacheForTiny:

private MemoryRegionCache<?> cacheForTiny(PoolArena<?> area, int normCapacity) {
int idx = PoolArena.tinyIdx(normCapacity);
if (area.isDirect()) {
return cache(tinySubPageDirectCaches, idx);
}
return cache(tinySubPageHeapCaches, idx);
}

PoolArena.tinyIdx(normCapacity)是找到tiny类型缓存数组的下标

继续跟tinyIdx:

static int tinyIdx(int normCapacity) {
return normCapacity >>> 4;
}

这里直接将normCapacity除以16, 通过前面的内容我们知道, tiny类型缓存数组中每个元素规格化的数据都是16的倍数, 所以通过这种方式可以找到其下标, 参考图5-2, 如果是16B会拿到下标为1的元素, 如果是32B则会拿到下标为2的元素

回到acheForTiny方法中:

if (area.isDirect()) 这里判断是否是分配堆外内存, 因为我们是按照堆外内存进行举例, 所以这里为true

再继续跟到cache(tinySubPageDirectCaches, idx)方法中:

private static <T> MemoryRegionCache<T> cache(MemoryRegionCache<T>[] cache, int idx) {
if (cache == null || idx > cache.length - 1) {
return null;
}
return cache[idx];
}

这里我们看到直接通过下标的方式拿到了缓存数组中的对象

回到PoolThreadCache的allocateTiny方法中:

boolean allocateTiny(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) {
return allocate(cacheForTiny(area, normCapacity), buf, reqCapacity);
}

拿到了缓存对象之后, 我们跟到allocate(cacheForTiny(area, normCapacity), buf, reqCapacity)方法中:

private boolean allocate(MemoryRegionCache<?> cache, PooledByteBuf buf, int reqCapacity) {
if (cache == null) {
return false;
}
boolean allocated = cache.allocate(buf, reqCapacity);
if (++ allocations >= freeSweepAllocationThreshold) {
allocations = 0;
trim();
}
return allocated;
}

这里通过cache.allocate(buf, reqCapacity)进行继续进行分配

再继续往里跟, 跟到内部类MemoryRegionCache的allocate(PooledByteBuf<T> buf, int reqCapacity)方法中:

public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) {
Entry<T> entry = queue.poll();
if (entry == null) {
return false;
}
initBuf(entry.chunk, entry.handle, buf, reqCapacity);
entry.recycle();
++ allocations;
return true;
}

这里首先通过queue.poll()这种方式弹出一个entry, 我们之前的小节分析过, MemoryRegionCache维护着一个队列, 而队列中的每一个值是一个entry

我们简单看下Entry这个类:

static final class Entry<T> {
final Handle<Entry<?>> recyclerHandle;
PoolChunk<T> chunk;
long handle = -1; //代码省略
}

这里重点关注chunk和handle的这两个属性, chunk代表一块连续的内存, 我们之前简单介绍过, netty是通过chunk为单位进行内存分配的, 我们之后会对chunk进行剖析

handle相当于一个指针, 可以唯一定位到chunk里面的一块连续的内存, 之后也会详细分析

这样, 通过chunk和handle就可以定位ByteBuf中指定一块连续内存, 有关ByteBuf相关的读写, 都会在这块内存中进行

我们回到MemoryRegionCache的allocate(PooledByteBuf<T> buf, int reqCapacity)方法:

public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) {
Entry<T> entry = queue.poll();
if (entry == null) {
return false;
}
initBuf(entry.chunk, entry.handle, buf, reqCapacity);
entry.recycle();
++ allocations;
return true;
}

弹出entry之后, 通过initBuf(entry.chunk, entry.handle, buf, reqCapacity)这种方式给ByteBuf初始化, 这里参数传入我们刚才分析过的当前Entry的chunk和hanle

因为我们分析的tiny类型的缓存对象是SubPageMemoryRegionCache类型,所以我们继续跟到SubPageMemoryRegionCache类的initBuf(entry.chunk, entry.handle, buf, reqCapacity)方法中:

protected void initBuf(
PoolChunk<T> chunk, long handle, PooledByteBuf<T> buf, int reqCapacity) {
chunk.initBufWithSubpage(buf, handle, reqCapacity);
}

这里的chunk调用了initBufWithSubpage(buf, handle, reqCapacity)方法, 其实就是PoolChunk类中的方法

我们继续跟initBufWithSubpage:

void initBufWithSubpage(PooledByteBuf<T> buf, long handle, int reqCapacity) {
initBufWithSubpage(buf, handle, bitmapIdx(handle), reqCapacity);
}

这里有关bitmapIdx(handle)相关的逻辑, 会在后续的章节进行剖析, 这里继续往里跟:

private void initBufWithSubpage(PooledByteBuf<T> buf, long handle, int bitmapIdx, int reqCapacity) {
assert bitmapIdx != 0;
int memoryMapIdx = memoryMapIdx(handle);
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
assert subpage.doNotDestroy;
assert reqCapacity <= subpage.elemSize;
buf.init(
this, handle,
runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize, reqCapacity, subpage.elemSize,
arena.parent.threadCache());
}

这里我们先关注init方法, 因为我们是以PooledUnsafeDirectByteBuf为例, 所以这里走的是PooledUnsafeDirectByteBuf的init方法

跟进init方法:

void init(PoolChunk<ByteBuffer> chunk, long handle, int offset, int length, int maxLength,
PoolThreadCache cache) {
super.init(chunk, handle, offset, length, maxLength, cache);
initMemoryAddress();
}

首先调用了父类的init方法, 再跟进去:

void init(PoolChunk<T> chunk, long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
//初始化
assert handle >= 0;
assert chunk != null;
//在哪一块内存上进行分配的
this.chunk = chunk;
//这一块内存上的哪一块连续内存
this.handle = handle;
memory = chunk.memory;
this.offset = offset;
this.length = length;
this.maxLength = maxLength;
tmpNioBuf = null;
this.cache = cache;
}

这里将PooledUnsafeDirectByteBuf的各个属性进行了初始化

this.chunk = chunk 这里初始化了chunk, 代表当前的ByteBuf是在哪一块内存中分配的

this.handle = handle 这里初始化了handle, 代表当前的ByteBuf是这块内存的哪个连续内存

有关offset和length, 我们会在之后的小节进行分析, 在这里我们只需要知道, 通过缓存分配ByteBuf, 我们只需要通过一个chunk和handle, 就可以确定一块内存

以上就是通过缓存分配ByteBuf对象的过程

我们回到MemoryRegionCache的allocate(PooledByteBuf<T> buf, int reqCapacity)方法:

public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) {
Entry<T> entry = queue.poll();
if (entry == null) {
return false;
}
initBuf(entry.chunk, entry.handle, buf, reqCapacity);
entry.recycle();
++ allocations;
return true;
}

分析完了initBuf方法, 再继续往下看

entry.recycle()这步是将entry对象进行回收, 因为entry对象弹出之后没有再被引用, 可能gc会将entry对象回收, netty为了将对象进行循环利用, 就将其放在对象回收站进行回收

我们跟进recycle方法:

void recycle() {
chunk = null;
handle = -1;
recyclerHandle.recycle(this);
}

chunk = null和handle = -1表示当前Entry不指向任何一块内存

recyclerHandle.recycle(this) 将当前entry回收, 有关对象回收站, 我们会在后面的章节详细剖析

以上就是命中缓存的流程, 因为这里我们是假设缓中有值的情况下进行分配的, 如果第一次分配, 缓存中是没有值的, 那么在缓存中没有值的情况下, netty是如何进行分配的呢?我们再之后的小节会进行剖析

上一节: directArena分配缓冲区概述

下一节: page级别的内存分配

Netty源码分析第5章(ByteBuf)---->第6节: 命中缓存的分配的更多相关文章

  1. Netty源码分析第5章(ByteBuf)---->第4节: PooledByteBufAllocator简述

    Netty源码分析第五章: ByteBuf 第四节: PooledByteBufAllocator简述 上一小节简单介绍了ByteBufAllocator以及其子类UnPooledByteBufAll ...

  2. Netty源码分析第5章(ByteBuf)---->第5节: directArena分配缓冲区概述

    Netty源码分析第五章: ByteBuf 第五节: directArena分配缓冲区概述 上一小节简单分析了PooledByteBufAllocator中, 线程局部缓存和arean的相关逻辑, 这 ...

  3. Netty源码分析第5章(ByteBuf)---->第7节: page级别的内存分配

    Netty源码分析第五章: ByteBuf 第六节: page级别的内存分配 前面小节我们剖析过命中缓存的内存分配逻辑, 前提是如果缓存中有数据, 那么缓存中没有数据, netty是如何开辟一块内存进 ...

  4. Netty源码分析第5章(ByteBuf)---->第8节: subPage级别的内存分配

    Netty源码分析第五章: ByteBuf 第八节: subPage级别的内存分配 上一小节我们剖析了page级别的内存分配逻辑, 这一小节带大家剖析有关subPage级别的内存分配 通过之前的学习我 ...

  5. Netty源码分析第5章(ByteBuf)---->第10节: SocketChannel读取数据过程

    Netty源码分析第五章: ByteBuf 第十节: SocketChannel读取数据过程 我们第三章分析过客户端接入的流程, 这一小节带大家剖析客户端发送数据, Server读取数据的流程: 首先 ...

  6. Netty源码分析第5章(ByteBuf)---->第1节: AbstractByteBuf

    Netty源码分析第五章: ByteBuf 概述: 熟悉Nio的小伙伴应该对jdk底层byteBuffer不会陌生, 也就是字节缓冲区, 主要用于对网络底层io进行读写, 当channel中有数据时, ...

  7. Netty源码分析第5章(ByteBuf)---->第2节: ByteBuf的分类

    Netty源码分析第五章: ByteBuf 第二节: ByteBuf的分类 上一小节简单介绍了AbstractByteBuf这个抽象类, 这一小节对其子类的分类做一个简单的介绍 ByteBuf根据不同 ...

  8. Netty源码分析第5章(ByteBuf)---->第3节: 缓冲区分配器

    Netty源码分析第五章: ByteBuf 第三节: 缓冲区分配器 缓冲区分配器, 顾明思议就是分配缓冲区的工具, 在netty中, 缓冲区分配器的顶级抽象是接口ByteBufAllocator, 里 ...

  9. Netty源码分析第5章(ByteBuf)---->第9节: ByteBuf回收

    Netty源码分析第五章: ByteBuf 第九节: ByteBuf回收 之前的章节我们提到过, 堆外内存是不受jvm垃圾回收机制控制的, 所以我们分配一块堆外内存进行ByteBuf操作时, 使用完毕 ...

随机推荐

  1. CF613D:Kingdom and its Cities(树形DP,虚树)

    Description 一个王国有n座城市,城市之间由n-1条道路相连,形成一个树结构,国王决定将一些城市设为重要城市. 这个国家有的时候会遭受外敌入侵,重要城市由于加强了防护,一定不会被占领.而非重 ...

  2. BZOJ3611:[HEOI2014]大工程(树形DP,虚树)

    Description 国家有一个大工程,要给一个非常大的交通网络里建一些新的通道.  我们这个国家位置非常特殊,可以看成是一个单位边权的树,城市位于顶点上.  在 2 个国家 a,b 之间建一条新通 ...

  3. Python自动化之迭代器不能在迭代的时候更改值

    除列表外的其他序列都是不可变的, 所以危险就发生在这里. 一个序列的迭代器只是记录你当前到达第多少个元素, 所以如果你在迭代时改变了元素, 更新会立即反映到你所迭代的条目上.在迭代字典的 key 时, ...

  4. Kafka设计解析(十五)Kafka controller重设计

    转载自 huxihx,原文链接 Kafka controller重设计 目录 一.Controller是做什么的 二.Controller当前设计 三.Controller组成 四.Controlle ...

  5. 如何使用eclipse运行简单的java程序

    打开eclipse,选择“file——new——Java project”   为我们的java项目取一个名字,然后点击完成.   这时候左侧列表就有了我们刚才新建的java项目,点开项目,在src目 ...

  6. Python学习笔记系列——九九乘法表&猜大小

    再重新捡起Python,数据库短时间之内已经没啥看的了,不知道今年结束之前能不能Python入门,一直认为自己是没有编程思想的... 1.九九乘法表 #九九乘法表实现的一种方式之一 def Multi ...

  7. Manifest XML signature is not valid(安装ClickOnce签名未通过验证)

    转载:http://stackoverflow.com/questions/12826798/manifest-xml-signature-is-not-valid 安装时,我的问题:  PLATFO ...

  8. MapReduce开发程序,运行环境配置

    Hadoop主机:linux 开发环境主机:Win7 + Itellij 本地运行 1. 下载hadoop安装包,放到本地目录中. 2. 配置环境变量$HADOOP_HOME及$PATH=$HADOO ...

  9. freemarker时间转换

    Freemarker日期函数处理[转] (2012-08-01 14:32:13)   string(当和一个日期值一起使用) 这个内置标签用指定的格式把日期转换成字符串,(把默认的格式用FreeMa ...

  10. ACM1001:Sum Problem

    Problem Description In this problem, your task is to calculate SUM(n) = 1 + 2 + 3 + ... + n.   Input ...