当客户端建立连接后,客户端向服务端发送消息,服务端需要读取消息,一般在常见的在worker线程中会有如下操作:
NioByteUnsafe
AbstractNioByteChannel 进行实际read
public final void read() {
final ChannelConfig config = config();
if (shouldBreakReadReady(config)) {
clearReadPending();
return;
}
final ChannelPipeline pipeline = pipeline();
//对应 DefaultChannelConfig中 ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
//不同平台和不同环境有不同设置
final ByteBufAllocator allocator = config.getAllocator();
// channelConfig中 rcvBufAllocator 为 AdaptiveRecvByteBufAllocator,
// 继承DefaultMaxMessagesRecvByteBufAllocator 能够设置每次最大读取数据大小
// AdaptiveRecvByteBufAllocator HandleImpl
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
byteBuf = allocHandle.allocate(allocator);
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
}
allocHandle.incMessagesRead(1);
readPending = false;
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close, allocHandle);
} finally {channelReadComplete(...) method
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
}
在ByteBufUtil
中进行实际的初始化,在其静态代码块中实现:
static {
String allocType = SystemPropertyUtil.get(
"io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}
DEFAULT_ALLOCATOR = alloc;
THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 0);
logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);
MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024);
}
我们这里以PooledByteBufAllocator
分配堆内存为例说明为例进行说明。
当进行内存分配allocate
时候,实际上是调用了内存分配器进行分配,
这里可以通过系统变量io.netty.allocator.type
来指定内存分配类型,取值unpooled pooled
,如果没有指定的话且不是安卓平台,则返回的是pooled
在allocHandle.allocate(allocator);
调用的是Allocatetor.ioBuffer
进行操作,这里首先判断是否能够支持对外内存分配,如果能够支持对外直接内存分配,则优先使用对外直接内存
public ByteBuf ioBuffer(int initialCapacity) {
if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
return directBuffer(initialCapacity);
}
return heapBuffer(initialCapacity);
}
// PooledByteBufAllocator.java
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = threadCache.get();
PoolArena<ByteBuffer> directArena = cache.directArena;
final ByteBuf buf;
if (directArena != null) {
buf = directArena.allocate(cache, initialCapacity, maxCapacity);
} else {
buf = PlatformDependent.hasUnsafe() ?
UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return toLeakAwareBuffer(buf);
}
华丽的分割线
在这之前,先说下Netty中池化的处理,以PooledDirectByteBuf
举例,其内部维护了一个对象池化处理器:
private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
new ObjectCreator<PooledDirectByteBuf>() {
@Override
public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
return new PooledDirectByteBuf(handle, 0);
}
});
//ObjectPool.newPool定义如下:
public static <T> ObjectPool<T> newPool(final ObjectCreator<T> creator) {
return new RecyclerObjectPool<T>(ObjectUtil.checkNotNull(creator, "creator"));
}
每次如果要创建一个新的PooledByteBuf
时,通过:
static PooledDirectByteBuf newInstance(int maxCapacity) {
PooledDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
其中RECYCLER.get();
调用对应的是在ObjectPool
中定义的RecyclerObjectPool
的Recycler<T> recycler;
循环回收器
private static final class RecyclerObjectPool<T> extends ObjectPool<T> {
private final Recycler<T> recycler;
RecyclerObjectPool(final ObjectCreator<T> creator) {
recycler = new Recycler<T>() {
@Override
protected T newObject(Handle<T> handle) {
return creator.newObject(handle);
}
};
}
@Override
public T get() {
return recycler.get();
}
}
Recyler
中get
定义如下:
public final T get() {
if (maxCapacityPerThread == 0) { //当线程最大容量为0的时候,返回对象,但是不回收
return newObject((Handle<T>) NOOP_HANDLE);
}
Stack<T> stack = threadLocal.get(); //线程本地变量获取线程对应的栈
DefaultHandle<T> handle = stack.pop();
if (handle == null) {
handle = stack.newHandle();
handle.value = newObject(handle); //没有可循环利用的对象的时候,调用对象创建器创建一个新的对象,并把循环处理器 handle一块传递过去
}
return (T) handle.value;
}
stack.newHandler()
返回的是默认带有循环利用的DefaultHandler
:
DefaultHandle<T> newHandle() {
return new DefaultHandle<T>(this);
}
当对象释放,可以循环回收的时候,调用Recyler.recyle
进行回收:
public final boolean recycle(T o, Handle<T> handle) {
if (handle == NOOP_HANDLE) {
return false;
}
DefaultHandle<T> h = (DefaultHandle<T>) handle;
if (h.stack.parent != this) {
return false;
}
h.recycle(o);
return true;
}
//DefaultHandle中回收处理
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
Stack<?> stack = this.stack;
if (lastRecycledId != recycleId || stack == null) {
throw new IllegalStateException("recycled already");
}
stack.push(this);
}
从上面代码可以发现,对象池里维护了一个Recyler回收器,回收器里面为每个线程维护了一个线程局部变量Stack
栈结构,里面的对象是DefaultHandle
对象,来进行实际的回收处理,当需要从对象池中获取对象时,
首先判断当前线程对象的Stack
中能否获取,如果不能,则通过定义的ObjectCreator.newObject
创建一个新的对象。
当对象需要进行回收时,先判断handle是否是需要回收,如果不是,则忽略。否则,将回收的对象放入当前线程本地变量的Stack
中去,这样完成了对象池的循环利用。
进行内存分配的时候首先从当前线程本地缓存中获取PoolThreadCache
,获取对应PoolArena
,这里的threadCache 实现为PoolThreadLocalCache
:
// PoolThreadLocalCache.java
protected synchronized PoolThreadCache initialValue() {
final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas);
final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas);
final Thread current = Thread.currentThread();
if (useCacheForAllThreads || current instanceof FastThreadLocalThread) {
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;
}
return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0);
}
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;
}
这里会从PooledByteBufAllocator
的arenas
数组中选取一个空闲最大的去进行分配,这里的arenas
数组大小取值如下:
final int defaultMinNumArena = NettyRuntime.availableProcessors() * 2;
DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
SystemPropertyUtil.getInt(
"io.netty.allocator.numDirectArenas",
(int) Math.min(
defaultMinNumArena,
PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));
可以看到这里默认情况下,取值应该是CPU*2,和前面说的LoopGroup的线程数一样,能够保证一个线程一个arena.
获取到PoolArena
之后,然后会通过PoolArena
进行内存的分配。
PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
PooledByteBuf<T> buf = newByteBuf(maxCapacity);
allocate(cache, buf, reqCapacity);
return buf;
}
protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) {
if (HAS_UNSAFE) {
return PooledUnsafeDirectByteBuf.newInstance(maxCapacity);
} else {
return PooledDirectByteBuf.newInstance(maxCapacity);
}
}
static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
PooledUnsafeDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
这里首先是获取一个PooledByteBuf
实例,这个对象实例也是会进行池化处理,返回了一个PooledUnsafeDirectByteBuf
。
需要注意的是,这个对象是对堆外内存做了一次封装,它持有了一个堆外内存的内存地址,根据这个地址来进行内存的写入和读取
获取到一个PooledByteBuf
之后,然后进行内存申请分配,将分配到的内存关联到PooledByteBuf
上。
Netty中通过 PoolArena
进行内存的管理分配:
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
final int normCapacity = normalizeCapacity(reqCapacity);
if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
int tableIdx;
PoolSubpage<T>[] table;
boolean tiny = isTiny(normCapacity);
if (tiny) { // < 512
if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
return;
}
tableIdx = tinyIdx(normCapacity);
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;
if (s != head) {
assert s.doNotDestroy && s.elemSize == normCapacity;
long handle = s.allocate();
assert handle >= 0;
s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
incTinySmallAllocation(tiny);
return;
}
}
synchronized (this) {
allocateNormal(buf, reqCapacity, normCapacity);
}
incTinySmallAllocation(tiny);
return;
}
if (normCapacity <= chunkSize) {
if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
return;
}
synchronized (this) {
allocateNormal(buf, reqCapacity, normCapacity);
++allocationsNormal;
}
} else {
allocateHuge(buf, reqCapacity);
}
}
这里首先一上来就会通过normalizeCapacity
对待申请的内存空间大小进行规范化,我们来看下是怎么规范处理的:
int normalizeCapacity(int reqCapacity) {
checkPositiveOrZero(reqCapacity, "reqCapacity");
if (reqCapacity >= chunkSize) {
return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity);
}
if (!isTiny(reqCapacity)) { // >= 512
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;
}
assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0;
return normalizedCapacity;
}
// directMemoryCacheAlignment默认为0
if (directMemoryCacheAlignment > 0) {
return alignCapacity(reqCapacity);
}
if ((reqCapacity & 15) == 0) {
return reqCapacity;
}
return (reqCapacity & ~15) + 16;
}
可以看到,如果是 <512,那么就看能否被16整除,如果能,直接返回,否则会向上取其16的倍数值,而如果>=512,则是会成倍扩充,即512 -> 1024 ->2048 -> 4096->8192
这样扩充,到这里就将请求的内存大小进行了调整规范,在Netty中,按照如下规格对北村进行了划分:
Tiny < 512B
512B <= Small < 8KB (pageSize)
8KB <= Normal <= 16MB(chunkSize)
Huge > 16MB (不会进行池化缓存,分配完,直接回收)
这里首先看本地线程缓存是否能够分配,如果能够分配那么直接分配,这块我们回头说。
如果本地线程无法分配,那么会尝试在PoolArena
的SubpagePools数组上分配,会根据调整后的请求内存大小,找到在数组中的下标,在PoolArena
中有如下两个数组:
private final PoolSubpage<T>[] tinySubpagePools;
private final PoolSubpage<T>[] smallSubpagePools;
而我们前面分析,调整后的内存主要分为两种<512
和 >=512&<8192
,其中<512
的规格都是按照16的倍数增长,而>=512&<8192
则是512 -> 1024 ->2048 -> 4096->8192扩充,因此对于<512
的规格,将normCapacity >>> 4就能够得到这个规格在tinySubpagePools
的下标,而对于>=512&<8192
则是先normCapacity>>>10,相当于除了1024,但是这里是成倍增加,所以如果不是0,还是需要除以2
static int tinyIdx(int normCapacity) {
return normCapacity >>> 4;
}
static int smallIdx(int normCapacity) {
int tableIdx = 0;
int i = normCapacity >>> 10;
while (i != 0) {
i >>>= 1;
tableIdx ++;
}
return tableIdx;
}
从上面的查找SubpagePools元素,我们发现,在PoolArena中tinySubpagePools和smallSubpagePools中每个元素都是代表了一个规格的内存,而PoolSubpage实际上是一个链表,这样PoolArena将同样规格的内存放在了PoolSubpage的一个链上
,如果能够在这个PoolSubpage上无法分配,在PoolArena上维护的几个PoolChunkList上分配,如果还是无法分配,那么会生成一个新的PoolChunk
上分配。
在Netty底层,内存的实际分配和管理是通过PoolChunk来实现的,PoolChunk申请到的内存,实际上一段连续的内存空间,这个内存被切分成了很多块,每块会有标志位表示是否已经被分配。每次获取指定大小的内存时,并不是申请多大就给多大,而是会进行大小标准化,这样会造成一些空间的浪费,但是这样方便管理维护。
PoolChunk中会将这连续的内存空间划分为多个page,默认情况下PoolChunk大小为16MB,每个page为8KB,PoolChunk会在其内部维护一颗平衡二叉树,
根节点2^(24-0B)=16MB,然后其子节点为 2^(24-1)B=8MB
大小,这样,这个二叉树一共12层,最底层的叶子节点,每个节点的大小就是2^(24-11)B=8KB
这样,如果我们要获取一个8KB大小,直接在叶子节点找一个,并将其状态置为已经分配,如果想要一个4MB大小,那么直接在第2层找一个可用的节点,通过公式:
depth = 11-(log2(normCapacity)-13
,我们能够很轻松找到需要的内存大小在这个二叉树的哪一层,找到这层后,其子节点需要设置为已经分配状态,父节点也要更新对应状态。
在PoolChunk中,有两个数组来维护这个信息:
private final byte[] memoryMap;
private final byte[] depthMap;
我们看下这几个参数是在Netty中是怎么获取的:
// PooledByteBufAllocator.java
int defaultPageSize = SystemPropertyUtil.getInt("io.netty.allocator.pageSize", 8192);
int defaultMaxOrder = SystemPropertyUtil.getInt("io.netty.allocator.maxOrder", 11);// 8192 << 11 = 16 MiB per chunk
DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
SystemPropertyUtil.getInt(
"io.netty.allocator.numDirectArenas",
(int) Math.min(
defaultMinNumArena,
PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));
DEFAULT_TINY_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.tinyCacheSize", 512);
DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.smallCacheSize", 256);
DEFAULT_NORMAL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.normalCacheSize", 64);
这里的memoryMap和depthMap初始逻辑如下:
maxSubpageAllocs = 1 << maxOrder;
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
可以看到,这里memoryMap和depthMap数组的大小是2^12=4096,memoryMap和depthMap初始里面都是维护二叉树中每个节点的位置和其对应的层高,比如memoryMap[1]=0 memoryMap[2]=1。
这个二叉树的大致结构如下:
首先会在本地线程缓存PoolThreadCache
中尝试分配,Netty中给每个本地线程缓存了三个数组(实际是六个,heap和direct各三个)
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<byte[]>[] normalHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;
分别对应了tiny、samll、normal
这三个内存级别。而这三个数组大小通过如下方式获取:
DEFAULT_TINY_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.tinyCacheSize", 512);
DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.smallCacheSize", 256);
DEFAULT_NORMAL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.normalCacheSize", 64);
我们看下具体申请内存的过程,首先会根据请求待分配缓存的大小从上述对应的数组中查询,
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
final int normCapacity = normalizeCapacity(reqCapacity);
if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
int tableIdx;
PoolSubpage<T>[] table;
boolean tiny = isTiny(normCapacity);
if (tiny) { // < 512
if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
return;
}
tableIdx = tinyIdx(normCapacity);
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, null, handle, reqCapacity);
incTinySmallAllocation(tiny);
return;
}
}
synchronized (this) {
allocateNormal(buf, reqCapacity, normCapacity);
}
incTinySmallAllocation(tiny);
return;
}
if (normCapacity <= chunkSize) {
if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
return;
}
synchronized (this) {
allocateNormal(buf, reqCapacity, normCapacity);
++allocationsNormal;
}
} else {
allocateHuge(buf, reqCapacity);
}
}
如果在缓存中无法找到对应大小的内存空间,则会尝试在PoolChunkList上分配,如果还不行,最终会生成一个新的PoolChunk在其上面进行分配:
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
q075.allocate(buf, reqCapacity, normCapacity)) {
return;
}
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
boolean success = c.allocate(buf, reqCapacity, normCapacity);
assert success;
qInit.add(c);
}
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
q075.allocate(buf, reqCapacity, normCapacity)) {
return;
}
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
boolean success = c.allocate(buf, reqCapacity, normCapacity);
assert success;
qInit.add(c);
}
// PoolChunk.java
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
final long handle;
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
handle = allocateRun(normCapacity);
} else {
handle = allocateSubpage(normCapacity);
}
if (handle < 0) {
return false;
}
ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
initBuf(buf, nioBuffer, handle, reqCapacity);
return true;
}
在PoolChunk上进行分配的时候,会分两种情况,>=pageSize(8KB)
和小于8KB的情况:
>8KB 情况分配:
private long allocateRun(int normCapacity) {
int d = maxOrder - (log2(normCapacity) - pageShifts); // 对应内存块大小在二叉树的哪一层
int id = allocateNode(d);
if (id < 0) {
return id;
}
freeBytes -= runLength(id);
return id;
}
private int allocateNode(int d) {
int id = 1;
int initial = - (1 << d); // has last d bits = 0 and rest all = 1
byte val = value(id);
if (val > d) { // 表明当前没有可以分配的空间
return -1;
}
while (val < d || (id & initial) == 0) {// 每次将id左移一位,即向下移动一层
id <<= 1;
val = value(id);
if (val > d) {
id ^= 1;
val = value(id);
}
}
byte value = value(id); //找到节点高度值
assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
value, id & initial, d);
setValue(id, unusable); // 标识相关节点不可用
updateParentsAlloc(id); // 标识父节点不可用
return id; // 返回id,1 ~ 2048*2 -1
}
private byte value(int id) {
return memoryMap[id];
}
private void setValue(int id, byte val) {
memoryMap[id] = val;
}
可以看到,这里主要就是通过memoryMap
来判断对应层节点和其下面的子节点是否有可用的待分配内存。这里我们讨论的是>=8KB内存大小的分配,在Netty中,PoolChunk每次申请都是16MB,然后会维护上面我们说的二叉树,借助这颗树,我们很容易的能对 >=8KB的内存大小找到一个合适位置
可以看到,这里
- 首先通过待分配的内存大小
normCapacity
获取其在二叉树哪一层 - 从二叉树的根节点开始判断当前节点及其子节点是否有空闲空间,首先判断根节点也就是memoryMap[1]的值是否大于当前节点的值,如果大于,表示下面没有该大小的可分配内存。
- 然后从根节点,每次将id左移一位,即按照,1,2,4,8,16这种方式,遍历,如果节点不可用,则通过id ^=1这种方式,来查询右兄弟节点及其子节点是否有可用(因为这时候该节点及其下面没有待分配大小的内存可用)如果满足条件
遍历节点的层高小于待分配内存大小的层高
继续向下遍历,这样一直遍历完之后,如果有可用内存块,则会被找到 - 将对应内存块节点设置为不可用,Netty中很简单,就是将对应节点的值设置为总层高+·1
memoryMap[id]=12
,这个时候是表明,当前节点及其所有子节点都不可用 - 设置父节点状态,这里会比较当前节点和兄弟节点memoryMap中保存的值的大小,用小的那一个设置父节点的memoryMap中的值(小的表明当前节点或者子节点还有节点可以分配,如果节点的memoryMap值=12,表明当前节点及其子节点都不可用(默认最高只有11层))
这里我们在贴一下上面的二叉树结构。
这样对于>=8KB的大小,我们借助这颗二叉树,很方便的就能够分配好,分配好之后,接下来会对PooledByteBuf
进行初始化,这个我们后面再说,
<=8KB 分配
private long allocateSubpage(int normCapacity) {
// 这里主要是从PoolArena获取一个同步变量,上锁
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
int d = maxOrder;
synchronized (head) {
int id = allocateNode(d);
if (id < 0) {
return id;
}
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;
freeBytes -= pageSize;
int subpageIdx = subpageIdx(id);
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null) {
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}
return subpage.allocate();
}
}
private int subpageIdx(int memoryMapIdx) {
return memoryMapIdx ^ maxSubpageAllocs; // maxSubpageAllocs=2048
}
可以看到,这里和>8KB的分配不同,>8KB是从根节点开始查找,而这里<=8KB是直接从叶子节点开始查找,因为叶子节点是这颗二叉树里面代表最小的内存块(8KB),我们看到,这里首先会根据返回的二叉树的节点ID(叶子节点ID),通过上面的图我们知道,二叉树的叶子节点一共是2048个块,而subpages
数组的大小也是2048,也就是subpages
数组每个元素正好对应到二叉树上的一个叶子节点。
需要注意的是这里和>8KB一样调用了allocateNode方法,即将叶子节点给PoolSubpage之后,该节点及其父节点就会更新节点的状态,后续无法分配
而这里memoryMapIdx ^ maxSubpageAllocs
可以理解为将memoryMapIdx -2048
正好将每个叶子节点在二叉树中的ID转换成在subpages
数组中的下标。
这样我们通过返回叶子节点的id能够在subpages
数组数组中找到对应的PoolSubpage
。
如果找到PoolSubpage
为空,则进行实例化:
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
init(head, elemSize);
}
void init(PoolSubpage<T> head, int elemSize) {
doNotDestroy = true;
this.elemSize = elemSize;
if (elemSize != 0) {
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
}
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0; // 对bitmap数组中每个元素long的64位都初始化
}
}
addToPool(head);
}
private void addToPool(PoolSubpage<T> head) {
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
这里有一个点需要注意,这里的PoolSubpage
会加入到从PoolArena
的SubpagePools
数组找到的PoolSubpage的链表上,在找到的这个节点的后面插入。
这里面每次不管PoolSubpage有没有实例化,都会调用init
:
这里暂时看不懂没关系,我们看下,PoolSubpage怎么分配内存的:
long allocate() {
if (elemSize == 0) {
return toHandle(0);
}
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;
bitmap[q] |= 1L << r;
if (-- numAvail == 0) {
removeFromPool();
}
return toHandle(bitmapIdx);
}
private long toHandle(int bitmapIdx) {
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
好了,到这里我们来简单梳理下PoolSubpage
的分配逻辑,在PoolSubpage中有如下bitmap:
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
长度为pagesize / 16 / 64 ,这里采用bitmap来标识一个内存单元是否被占用,在 PoolSubpage最小的内存单元为16B,然后依次按照16递增,用bitmap数组,每个数组用long,64位,可以表示64个内存块的使用情况,bitmap默认长度为8192/16/64=8
。
我们需要注意的一点是,如果这里能够找到一个PoolSubpage
则表明在二叉树中的叶子节点是空闲的,并且之后这个二叉树节点会标记为已经分配,这个分配就是给了PoolSubpage。
每个PoolSubpage内存块的大小是固定的,在init的时候就会给他固定好的,init的时候传入的elemSize就是PoolSubpage中的内存块的大小
另外,在PooChunk
的时候能找到一些PoolSubpage不为null,这表明这个PoolSubpage
的二叉树叶子结点内存空闲了,可以分配了,这时候还是会调用init来重新设置内存块大小。
初始化的最后,会把这个PoolSubpage
加入到PoolArena
的SubpagePools
数组中一个内存规格的链表上。
另外这里可以看到,我们每次初始
PoolSubpage
的时候实际上并不一定是16B,有可能大于16B,但是Netty中给bitmap这个数组大小就是固定在了8192/16/64,这样省的每次内存块变化时频繁对数组进行扩缩
这里通过bitmap我们能够找到哪些内存块没有分配,这里int q = bitmapIdx >>> 6
能够获取对应的bitmapId在bitmap数组中的下标,然后bitmapIdx & 63
能够获取到这个bitmapId在bitmap数组中元素的long类型的第几位,然后通过bitmap[q] |= 1L << r;
进行左移,将该位设置为1,表示这个bitmap对应位置的内存块已经被分配。
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
bitmap[q] |= 1L << r;
另外这里有这样一段,我在下面给出说明
// 我们知道,bitmap中是用一个long64位来表示64个内存单元是否分配,这里maxNumElems >>> 6就是 maxNumElems / 64,
// 我们按照给定的内存规格划分的内存单元个数在除以64就是实际需要的bitmap的数组的长度,但是为了防止有些不是64的整数倍,而无符号左移会损失以为,
//这里单独判断划分的内存单元的个数能否被64整除,如果不能 +1 ,说白了就是先对待分配内存单元用64整除,得到一个数字,
// 然后看是不是能被64整除,不能再+1,netty中这么算就是为了能够贴近底层计算机运算逻辑,你在netty中很多地方能见到一般不会有常规运算,都是位运算
maxNumElems = numAvail = pageSize / elemSize;
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
}
这里我们在啰嗦下,看看怎么找可分配的bitmapId的:
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}
private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
// 取反,如果为0,表明各个位上都是1,这个64个内存单元已经分配完,不为0,表明还有可分配的空间
if (~bits != 0) {
return findNextAvail0(i, bits);
}
}
return -1;
}
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
// baseVal是bitmap下标i中内存单元的起始序号
final int baseVal = i << 6;
// 这里从bits的64位的最低位一次和1进行&运算,如果==0表明该位置上的内存单元未被分配,否则bits继续无符号右移一位,判断下一位是否被分配
for (int j = 0; j < 64; j ++) {
if ((bits & 1) == 0) {
// baseVal是bitmap下标i中内存单元的起始序号,与j进行或运算,得到可分配内存单元的序号
int val = baseVal | j;
if (val < maxNumElems) {
return val;
} else {
break;
}
}
// 将bits无符号右移一位,继续判断
bits >>>= 1;
}
return -1;
}
这里上面都写了注释,其实逻辑很简单,比如这个PoolSubpage的内存规格是16B,则一共可以分配的内存单元是8192/16=512个内存单元,需要多少个bitmap呢 ?512/64=8,能够被整除,然后一次开始bitmap中每个元素,也就是long的每位上是否有带分配的内存,比如在bitmap[1]上的long的第32位找到可以分配的,那么这个待分配的内存单元的序号就是1*64+(32-1),就是85位
另外从这个查找结果可以看到,PoolSubpage是从bitmap中0,1,2开始查找分配,每个long的64位上也是从0,1,2开始查找分配。好了,到这里我们大概了解了PoolSubpage
是怎么分配的。
到这里我们就理解了在Netty中如果在缓存中无法分配的化,通过新生成PoolChunk怎么分配>8KB & <=16MB
和>16B & <=8KB
是怎么分配的。
另外,我们在PoolChunk.allocateSubpage
的时候,首先是会根据内存规格从PoolArena对应的tinySubpagePools或smallSubpagePools中找到对应的内存规格的PoolSubpage链表的head,然后将其加入到其head.next中
,注意这个是刚分配出来的PoolSubpage,而在PoolArena开始的时候首先就会从tinySubpagePools或smallSubpagePools中找对应的PoolSubpage,如果有表示能够分配,不会新生成PoolChunk,这里新生成的加入到PoolSubpage链表中,后面就会从这个PoolSubpage继续分配。同时会把这个新生成的PoolChunk
放入到PoolArena的PoolChunkList
中去。
前面我们还有一个PoolThreadCache
上的分配还没讲,我们先看下Netty中怎么将内存释放的,因为PoolThreadCache
上的内存都是释放后加入的
当我们进行内存回收释放时,一般代码如下:
byteBuf.release()
//或者
ReferenceCountUtil.release(byteBuf)
其最终调用的是:PooledByteBuf.deallocate
:
protected final void deallocate() {
if (handle >= 0) {
final long handle = this.handle;
this.handle = -1;
memory = null;
chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
tmpNioBuf = null;
chunk = null;
recycle();
}
}
private void recycle() {
recyclerHandle.recycle(this);
}
可以看到,PoolByteBuf
对象会被回收,回收逻辑按照上面对象池的回收逻辑回收,实际内存的回收是交给了PoolArena
处理:
void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
if (chunk.unpooled) {
int size = chunk.chunkSize();
destroyChunk(chunk);
activeBytesHuge.add(-size);
deallocationsHuge.increment();
} else {
SizeClass sizeClass = sizeClass(normCapacity);
if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
return;
}
freeChunk(chunk, handle, sizeClass, nioBuffer, false);
}
}
对于池化和非池化的内存处理逻辑不一样,如果是非池化内存,则直接回收并释放,对于池化内存,会加入到缓存中去,如果缓存失败,则也会直接回收并释放。
加入缓存,调用了PoolThreadCache.add
:
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);
}
这里其实在PoolThreadCache
中内存的处理和PoolArena
中的tinySubpagePools
和smallSubpagePools
处理类似,只不过,这里增加了一个>8KB & ,=16MB
的规格,在PoolThreadCache
维护了如下6个数组,其实就是堆内核堆外各三个
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<byte[]>[] normalHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;
这里缓存到本队线程的时候先会根据内存规格获取对应的MemoryRegionCache
:
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();
}
}
private MemoryRegionCache<?> cacheForTiny(PoolArena<?> area, int normCapacity) {
int idx = PoolArena.tinyIdx(normCapacity);
if (area.isDirect()) {
return cache(tinySubPageDirectCaches, idx);
}
return cache(tinySubPageHeapCaches, idx);
}
private MemoryRegionCache<?> cacheForSmall(PoolArena<?> area, int normCapacity) {
int idx = PoolArena.smallIdx(normCapacity);
if (area.isDirect()) {
return cache(smallSubPageDirectCaches, idx);
}
return cache(smallSubPageHeapCaches, idx);
}
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);
}
可以看到这里对Tiny和Small级别都是通过直接调用PoolArena相应的id获取逻辑,也就说明,在PoolThreadCache中tinySubPageDirectCaches和smallSubPageDirectCaches数组与PoolArena中的tinySubpagePools和smallSubpagePools主要逻辑都是一样的,每个数组元素都是一个固定大小的内存规格单元的链表(PoolThreadCache中通过MpscArrayQueue保存关系)
只不过,这里另外在加入了一个Normal级别的内存规格单元。
在PoolArena中并没有借助PoolSubpage去进行Normal级别的内存单元分配,而是直接基于PoolChunk里面维护的二叉树去进行分配的。
到这里,我门大概讲完了Netty中的内存分配,这里我们总结一下:
- 在Netty助攻是通过
PooledByteBufAllocator
来进行内存分配,在PooledByteBufAllocator
维护了一个PoolArena<ByteBuffer>[] directArenas
,然后大小与worker线程一样,然后会将其每个元素关联到一个PoolThreadCache
本地线程变量中 - 每次分配都获取本地线程变量的
PoolArena
,由PoolArena
去进行内存申请,PoolArena
每次申请的时都是申请16MB大小内存(默认),然后交由PoolChunk
去管理,每次申请时都会对待分配的内存的大小进行统一规范处理,生成的PoolChunk会加入到PoolArena
的PoolChunkList
链表中去 PoolChunk
中对16MB的内存进行分段处理,形成如下二叉树结构:
- 如果分配内存规格>8KB则直接在二叉树上分配,分配完之后,会标记对应节点的状态为已分配状态
- 如果待分配的内存规格<=8KB 则会对二叉树的叶子结点在进行细分,
PoolChunk
中维护了一个PoolSubpage<T>[] subpages;
数组,里面每个元素代表的是一个内存单元,这时候会根据待分配内存单元,将可分配的叶子结点封装成一个PoolSubpage,里面会将待分配的内存单元规格,设置为PoolSubpage中每个内存块大小,同时根据内存块大小得到当前这个PoolSubpage有多少个可以分配的内存单元。另外会将生成的PoolSubpage加入到PoolArena
中对应的tinySubpagePools
或smallSubpagePools
中去 - 当内存释放的时候,会将释放的内存放入到本地线程缓存中去,与上面的
tinySubpagePools
或smallSubpagePools一样,本地线程缓存中也维护这样的数组,但是还多一个,是
normalDirectCaches, 规格为>8KB & <=16MB
- 在
PoolArena
中每次分配都会对待分配的内存大小规范化,Netty中内存规格如下:
Tiny < 512B
512B <= Small < 8KB (pageSize)
8KB <= Normal <= 16MB(chunkSize)
Huge > 16MB (不会进行池化缓存,分配完,直接回收)
当待分配内存<=16MB
的时候,首先是先从本地线程缓存中查找有没有合适的待分配内存,如果没有对于tiny和small级别的,则从PoolArena
的SubpagePools
去尝试分配,如果分配不成功,尝试从PoolArena
的ChunkList
中分配,如果还不成功,则生成一个新的Chunk去分配,对于>16MB的内存申请,直接分配,不缓存