long bits = bitmap[i];
if (~bits != 0) { //@3
return findNextAvail0(i, bits); //@4
}
}
return -1;
}
代码@2:遍历bitmap,根据当前的elmSize可以得知当前用来多少个long来代表总的长度。
代码@3:如果取反不为0,则说明用这个long代表的64个PoolSubpage未使用完,可用从这里获取一个,故进入到代码@4。
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
final int baseVal = i << 6; //@5
for (int j = 0; j < 64; j ++) { //遍历,一个long总代表64个PoolSubpage
if ((bits & 1) == 0) { //@6
int val = baseVal | j; //@8
if (val < maxNumElems) {
return val;
} else {
break;
}
}
bits >>>= 1; //@7
}
return -1;
}
代码@5:baseVal = i << 6,也就是i * 64,如果i在bitmap数组中的下标为0,baseVal则等于0,如果下标为1,则baseVal=64,如果下标为2,则baseVal=128。
代码@6:用bits&1判断是否等于0,也就是判断bits的低位是否被占用,如果为1,则表示被占用,如果为0,表示未被占用,如果结果为0,则可以返回该值了,如果不为0,则无符号向右移动1位,相当与去掉最后一位,继续比较。从这里就可以看出,一个long从低位开始被标记。我们举例说明一下,比如现在 用两个long类型可以标记所有的PoolSubpage,比如bitmap[0] = (00000000 00000000 00000000 00000000 00000000 00000000 00000000 0111111),表示已经分配了7个PoolSubpage。
代码 toHandle方法详解:
private long toHandle(int bitmapIdx) {
return 0xb000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
该值,主要是用一个long类型的值的低32位来表示 memoryMapIdx,用高32位表示 bitmapIdx,这里为什么需要与0xb000000000000000L进行或运算呢?据我的目前掌握的知识看,主要是将0映射为一个数字,主要来区分是在PoolSubpage中分配的,还是内存大于pageSize,在内存释放的时候,可以通过bitmaIdx >> 32 & 0x3FFFFFFF 得出原来的索引。
1.3.1.4 关于 PoolArena allocate代码@4 initBuf详解:
void initBuf(PooledByteBuf buf, long handle, int reqCapacity) {
int memoryMapIdx = (int) handle;//@1
int bitmapIdx = (int) (handle >>> Integer.SIZE); //@2
if (bitmapIdx == 0) {//@3
byte val = value(memoryMapIdx);
assert val == unusable : String.valueOf(val);
buf.init(this, handle, runOffset(memoryMapIdx), reqCapacity, runLength(memoryMapIdx));
} else {//@4
initBufWithSubpage(buf, handle, bitmapIdx, reqCapacity);
}
}
首先这里对入参 long handle做一个详解:如果需要分配的内存大于pageSize,则返回的高32为为0,低32位为memoryMap的下标id。具体参照下文的allocateRun方法讲解;如果需要分配的内存小于pageSize,则返回的高32为为bitmapIdx,低32位同样表示memoryMap的id。
代码@1:从long中获取memoryMapIdx。
代码@2:从long中获取bitmapIdx。
代码@3:如果bitmapIdx为0,偏移量就是PoolSubpage的偏移量。为什么呢?bitmapIdx为0在内存申请大于pageSize和小于pageSize时都会出现:
-
如果申请的内存数小于pageSize,bitmapIdx表示该pageSubpage是第一次分配。
-
如果申请的内存数大于pageSize,表明该节点所以子节点都是第一次被分片。故偏移量就是memory[id]所代表的偏移量。
代码@4:如果bitmapIdx不为0,需要计算偏移量,与PoolSubpage的elemSize相关,具体请看2)PoolChunk的initBufWithSubpage方法,该方法,最终还是要调用PooledByteBuf的init方法,分配内存,这里只是需要计算偏远量。
1、PooledByteBuf的 int方法
void init(PoolChunk chunk, long handle, int offset, int length, int maxLength) {
assert handle >= 0;
assert chunk != null;
this.chunk = chunk;
this.handle = handle;
memory = chunk.memory; // @1
this.offset = offset; // @2
this.length = length; // @3
this.maxLength = maxLength; // @4
setIndex(0, 0); //@5
tmpNioBuf = null;
initThread = Thread.currentThread();//@6
}
代码@1:PooledByteBuf的内存,直接指向PoolChunk的memory;
代码@2:offset,在memory的起始偏移量。
代码@3:memory的offset + length之间的内存被该PooledByteBuf占用,其他ByteBuf无法使用。
代码@4:maxLength的作用是什么呢?目前我的理解是,PooledByteBuf自动扩容时,只要最终长度不超过maxLength,就可以在当前的内存中完成,不需要去申请新的空间,再进行内存负责。
代码@5:初始化readIndex,writeIndex。
代码@6:记录该PooledByteBuf的初始化线程,方便本地线程池的使用。
2、PoolChunk的iinitBufWithSubpage方法详解
private void initBufWithSubpage(PooledByteBuf buf, long handle, int bitmapIdx, int reqCapacity) {
assert bitmapIdx != 0;
int memoryMapIdx = (int) handle;
PoolSubpage subpage = subpages[subpageIdx(memoryMapIdx)];
assert subpage.doNotDestroy;
assert reqCapacity <= subpage.elemSize;
buf.init(
this, handle,
runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize, reqCapacity, subpage.elemSize);
}
1.3.2 PoolChunk allocate关于代码@allocateRun,超过pageSize内存分配源码分析
/**
-
Allocate a run of pages (>=1)
-
@param normCapacity normalized capacity
-
@return index in memoryMap
*/
private long allocateRun(int normCapacity) {
int d = maxOrder - (log2(normCapacity) - pageShifts); //@1
int id = allocateNode(d); //@2
if (id < 0) {
return id;
}
freeBytes -= runLength(id); //@3
return id;
}
该方法的实现,就是要在平衡二叉树中要找到一个可以满足normCapacity的节点,从前文的介绍中得知,memoryMap[id]存放的是,该id能分配的最小深度代表的容量。超过pageSize的内存节点,肯定不是在叶子节点。如果让我实现查找合适id的算法,我想应该是这样的:
1、算成需要分配的内存大小是 pageSize的倍数,再直观点就是normalCapacity 是 pageSize 的倍数n,然后算成n是2的多少次幂,比如pageSize=4,而normalCapacity是16,得出的倍数是4,4是2的2次幂,最终要得到这个值。标记为r
2、然后从平衡二叉树,沿着最底层(高度为0,深度为maxOrder),向上找r级,即能知道这一层次的节点可以容纳下normaCapacity的节点。
代码@1:先算出normCapacity,2的对(2的幂)然后减去pageSize(2的幂),得出思路1的r的值,然后用maxOrder-r,就表示深度最大为maxOrder-r。
代码@2:沿着平衡二叉树,从根节点开始,寻找一个合适的节点。如果无法分配,返回-1。
allocateNode(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) { // unusable
return -1;
}
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
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); // mark as unusable
updateParentsAlloc(id);
return id;
}
1.3.3 关于PoolArena 2.2.2 allocate方法的代码@5allocateHuge方法详解
在netty内存管理中,如果超过chunkSize的内存,为大内存,不重复使用。
private void allocateHuge(PooledByteBuf buf, int reqCapacity) {
buf.initUnpooled(newUnpooledChunk(reqCapacity), reqCapacity);
}
PoolArena.HeapArena:
protected PoolChunk<byte[]> newUnpooledChunk(int capacity) {
return new PoolChunk<byte[]>(this, new byte[capacity], capacity);
}
PoolChunk非池管理的PoolChunk:
/** Creates a special chunk that is not pooled. */
PoolChunk(PoolArena arena, T memory, int size) {
unpooled = true;
this.arena = arena;
this.memory = memory;
memoryMap = null;
depthMap = null;
subpages = null;
subpageOverflowMask = 0;
pageSize = 0;
pageShifts = 0;
maxOrder = 0;
unusable = (byte) (maxOrder + 1);
chunkSize = size;
log2ChunkSize = log2(chunkSize);
maxSubpageAllocs = 0;
}
大内存的PoolChunk,不缓存使用,故内部不会再细化为PoolSubpage等数据结构。
void initUnpooled(PoolChunk chunk, int length) {
assert chunk != null;
this.chunk = chunk;
handle = 0;
memory = chunk.memory;
offset = 0;
this.length = maxLength = length;
setIndex(0, 0);
tmpNioBuf = null;
initThread = Thread.currentThread();
}
PooledByteBuf来初始化是,lengt,maxLenth等于需要申请的内存。看过tiny,small内存的分配后,大内存的分配比较简单,就不做每行代码的解读。不容易呀,Netty内存的分配就讲解完毕了。接下来注重分析两个方面:内存释放与PooledByteBuf动态扩容。
2、源码分析Netty内存释放
===============
在讲解Netty内存释放之前,我们还是简单的再回顾一下ReferenceCounted,Netty的ByteBuf继承该接口,这也表明,Netty内部的内存管理是基于引用计数来进行内存的回收的。具体的实现是AbstractReferenceCounted。所以,我们在使用PooledByteBuf时,用完后要记得调用release方法。在线程本地分配时会再次强调,如果PooledByteBuf在用完后,没有调用realse方法,是无法被线程共用的。内存的释放入口,是ByteBuf的relase方法,也就是AbstractReferenceCountedByteBuf的release方法:
public final boolean release(int decrement) {
if (decrement <= 0) {
throw new IllegalArgumentException(“decrement: " + decrement + " (expected: > 0)”);
}
for (;😉 {
int refCnt = this.refCnt;
if (refCnt < decrement) {
throw new IllegalReferenceCountException(refCnt, -decrement);
}
if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
if (refCnt == decrement) {
deallocate();
return true;
}
return false;
}
}
}
该方法的方式是循环利用CAS将引用减少,直到引用为0,则调用释放方法,ByteBuf的内存释放,不同子类,不同的实现,故deallocate方法为抽象方法;
/**
- Called once {@link #refCnt()} is equals 0.
*/
protected abstract void deallocate();
3.1 PooledByteBuf deallocate方法详解
protected final void deallocate() {
if (handle >= 0) {
final long handle = this.handle;
this.handle = -1;
memory = null;
boolean sameThread = initThread == Thread.currentThread();
initThread = null;
chunk.arena.free(chunk, handle, maxLength, sameThread); //@1
recycle(); //@2
}
}
内存释放的基本实现:首先将PooledByteBuf内部的handle修改为-1,将memory设置为null,,然后释放占用的内存,然后将该PooledByteBuf放入对象池。从这里也可以看出,PooledByteBuf ,不仅ByteBuf内部持有的缓存区会被回收,连PooledByteBuf这个对象本身,也会放入到对象池,供重复利用(线程级)。
2.1关于PooledByteBuf deallocate 代码@1 PoolArena的free方法,内存释放
/**
-
@param chunk
-
@param handle,内存分配,保存着bitmapIdx,memoryMapIdx
-
@param normaCapacity 申请的内存(释放的内存,取值为PooledByteBuf的 maxLenth)
-
@param 释放该PooledByteBuf的线程释放时创建该PooledByteBuf的线程
*/
void free(PoolChunk chunk, long handle, int normCapacity, boolean sameThreads) {
if (chunk.unpooled) { //@1
destroyChunk(chunk);
} else {
if (sameThreads) { //@2
PoolThreadCache cache = parent.threadCache.get();
if (cache.add(this, chunk, handle, normCapacity)) {
// cached so not free it.
return;
}
}
synchronized (this) { //@3
chunk.parent.free(chunk, handle);
}
}
}
如果是非池的内存,也就是大内存,直接释放,如果释放的线程是创建该PooledByteBuf的线程,则放入线程本地变量中,重复利用,此时不需要释放。如果不是,则释放内存。
代码@1:我们看一下PoolArena.HeapArena和DirectArena的分别实现:
PoolArena.HeapArena
protected void destroyChunk(PoolChunk<byte[]> chunk) {
// Rely on GC.
}
PoolArena.DirectArena:
protected void destroyChunk(PoolChunk chunk) {
PlatformDependent.freeDirectBuffer(chunk.memory); //释放堆外内存
}
代码@2:放入本地线程缓存中,这个稍后重点关注。
代码@3:释放内存,这里的内存释放与第一步的内存释放不同,第一步的内存释放,是直接将内存还给JVM堆、或操作系统。而这里的是在PoolChunk中进行标记与释放而已。chunk.parent指的是该PoolChunk的PoolChunkList对象。先重点进入到PoolChunk的free方法。
2.2 关于PoolArena.free方法 代码@3的讲解,此处主要是调用PoolChunk方法进行内存的释放:
/**
-
Free a subpage or a run of pages
-
When a subpage is freed from PoolSubpage, it might be added back to subpage pool of the owning PoolArena
-
If the subpage pool in PoolArena has at least one other PoolSubpage of given elemSize, we can
-
completely free the owning Page so it is available for subsequent allocations
-
@param handle handle to free
*/
void free(long handle) {
int memoryMapIdx = (int) handle; //@1
int bitmapIdx = (int) (handle >>> Integer.SIZE); //@2
if (bitmapIdx != 0) { // free a subpage //@3
PoolSubpage subpage = subpages[subpageIdx(memoryMapIdx)];
assert subpage != null && subpage.doNotDestroy;
if (subpage.free(bitmapIdx & 0x3FFFFFFF)) {
return;
}
}
freeBytes += runLength(memoryMapIdx); //@4
setValue(memoryMapIdx, depth(memoryMapIdx)); //@5
updateParentsFree(memoryMapIdx); //@6
}
代码@1:获取在memoryMap的下标。
代码@2:获取bitmapIdx。
代码@3:如果bitmapIdx不等于0,表示该内存是从PoolSubpage中直接分配的(内存申请时,小于pageSize)。此时内存的释放,直接调用PoolSubpage的free方法。
代码@4,@5,@6:如果申请的内存超过了pageSize,首先将freeBytes增加,然后在代码@5,将memoryMapIdx处的值更新为原先depth的值,代表可以重新分片深度为depth[id]的内存了,级联更新memoryMap,的父节点的释放状态。
2.3 关于 PoolArena.free方法,代码@3, PoolSubpage.free方法详解。
/**
-
@return {@code true} if this subpage is in use.
-
{@code false} if this subpage is not used by its chunk and thus it's OK to be released.
*/
boolean free(int bitmapIdx) {
if (elemSize == 0) {
return true;
}
int q = bitmapIdx >>> 6; //@1
int r = bitmapIdx & 63; //@2
assert (bitmap[q] >>> r & 1) != 0;
bitmap[q] ^= 1L << r; //@3
setNextAvail(bitmapIdx); //@4
if (numAvail ++ == 0) { //@5 start
addToPool();
return true;
}
if (numAvail != maxNumElems) {
return true;
} else {
// Subpage not in use (numAvail == maxNumElems)
if (prev == next) {
// Do not remove if this subpage is the only one left in the pool.
return true;
}
// Remove this subpage from the pool if there are other subpages left in the pool.
doNotDestroy = false;
removeFromPool();
return false;
} //@5 end
}
代码@1:求出bitmapIdx在bitmap数组中的下标。
代码@3:就是bitmapIdx在bitmap[q]下所对应的高位1,设置为0,表示可以再次被用来分配。
代码@4:将bitmapIdx设置为下一个可以用nextAvail,这时在nextAvail>0,分配时候就会直接先用释放的,保证内存的连续性。
代码@5:如果PoolSubpage有剩余空间后,加入到PoolArena的tinyPoolSubpages[]或smallPoolSubpages[]中。如果空间全部分配后,或一个都没分配,从PoolArena的tinyPoolSubpages[],smallPoolSubpages[]中移除。
3.3 关于PoolArena.free方法,代码@6,级联更新释放状态详解
/**
-
Update method used by free
-
This needs to handle the special case when both children are completely free
-
in which case parent be directly allocated on request of size = child-size * 2
-
@param id id
*/
private void updateParentsFree(int id) {
int logChild = depth(id) + 1;
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
logChild -= 1; // in first iteration equals log, subsequently reduce 1 from logChild as we traverse up
if (val1 == logChild && val2 == logChild) {
setValue(parentId, (byte) (logChild - 1));
} else {
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
}
id = parentId;
}
}
这个算法在理解了上文的updateParentsAllocator()方法后,不难理解,只要我们明确一点,memoryMap[id]=order,表示id代表的节点,可以胜任大于等于order深度的内存分配需要。
3、PooledByteBuf内存扩容
===================
在讲解这个问题的时候,不知道大家有没有注意到PooledByteBuf的maxLength属性?该属性有和作用,请看下文分解。
PooledByteBuf扩容算法,请看capacity(int newCapacity);
@Override
public final ByteBuf capacity(int newCapacity) {
ensureAccessible();
// If the request capacity does not require reallocation, just update the length of the memory.
if (chunk.unpooled) { //@1
if (newCapacity == length) {
return this;
}
} else {
if (newCapacity > length) { //@2
if (newCapacity <= maxLength) {
length = newCapacity;
return this;
}
} else if (newCapacity < length) { //@3
if (newCapacity > maxLength >>> 1) {
if (maxLength <= 512) {
if (newCapacity > maxLength - 16) {
length = newCapacity;
setIndex(Math.min(readerIndex(), newCapacity), Math.min(writerIndex(), newCapacity));
return this;
}
} else { // > 512 (i.e. >= 1024)
length = newCapacity;
setIndex(Math.min(readerIndex(), newCapacity), Math.min(writerIndex(), newCapacity));
return this;
}
}
} else {
return this;
}
}
// Reallocation required.
chunk.arena.reallocate(this, newCapacity, true); //@4
return this;
}
代码@1:如果是非池化的,并且新的容量等于length的话,可以直接返回,否则需要重新去申请内存。
代码@2:如果需要的内存大于length,此时newCapacity小于等于maxLength,则不需要扩容,如果需要的大小超过maxLength,则需要重新去申请内存。那这里的maxLength是什么呢?其实,如果PooledByteBuf的内存是在PoolSubpage中分配,那maxLength为PooledSubpage的内存区域中的总容量,PoolSubpage的MemoryRegion的总大小。也就是在是同一个时刻,一个PoolSubpage的MemoryRegion只能被一个PooledByteBuf所占用。
代码@3:如果需要申请的内存小于length,为了避免下一次需要扩容,再进一步判断,如果是tiny内存的话,判断newCapaciy与 maxLength-16的关系,如果不大于的话,本次也进行重新分配内存,然后维护PooledByteBuf的length,readerIndex,writerIndex。
代码@4:内存的重新分配 reallocate方法;
void reallocate(PooledByteBuf buf, int newCapacity, boolean freeOldMemory) {
if (newCapacity < 0 || newCapacity > buf.maxCapacity()) {
throw new IllegalArgumentException("newCapacity: " + newCapacity);
}
int oldCapacity = buf.length;
if (oldCapacity == newCapacity) {
return;
}
PoolChunk oldChunk = buf.chunk;
long oldHandle = buf.handle;
T oldMemory = buf.memory;
int oldOffset = buf.offset;
int oldMaxLength = buf.maxLength;
int readerIndex = buf.readerIndex();
int writerIndex = buf.writerIndex();
allocate(parent.threadCache.get(), buf, newCapacity); //@1
if (newCapacity > oldCapacity) { //@2 start
memoryCopy(
oldMemory, oldOffset,
buf.memory, buf.offset, oldCapacity);
} else if (newCapacity < oldCapacity) {
if (readerIndex < newCapacity) {
if (writerIndex > newCapacity) {
writerIndex = newCapacity;
}
memoryCopy(
oldMemory, oldOffset + readerIndex,
buf.memory, buf.offset + readerIndex, writerIndex - readerIndex);
} else {
readerIndex = writerIndex = newCapacity;
}
} // @2end
buf.setIndex(readerIndex, writerIndex);
if (freeOldMemory) {
free(oldChunk, oldHandle, oldMaxLength, buf.initThread == Thread.currentThread()); //@3
}
}
内存重新分配的算法如下:
首先重新申请内存,然后进行内存复制,最后释放原先的内存。
PoolArena.HeapArena的memoryCopy实现代码:
如何快速更新自己的技术积累?
- 在现有的项目里,深挖技术,比如用到netty可以把相关底层代码和要点都看起来。
- 如果不知道目前的努力方向,就看自己的领导或公司里技术强的人在学什么。
- 知道努力方向后不知道该怎么学,就到处去找相关资料然后练习。
- 学习以后不知道有没有学成,则可以通过面试去检验。
我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目
emoryCopy(
oldMemory, oldOffset,
buf.memory, buf.offset, oldCapacity);
} else if (newCapacity < oldCapacity) {
if (readerIndex < newCapacity) {
if (writerIndex > newCapacity) {
writerIndex = newCapacity;
}
memoryCopy(
oldMemory, oldOffset + readerIndex,
buf.memory, buf.offset + readerIndex, writerIndex - readerIndex);
} else {
readerIndex = writerIndex = newCapacity;
}
} // @2end
buf.setIndex(readerIndex, writerIndex);
if (freeOldMemory) {
free(oldChunk, oldHandle, oldMaxLength, buf.initThread == Thread.currentThread()); //@3
}
}
内存重新分配的算法如下:
首先重新申请内存,然后进行内存复制,最后释放原先的内存。
PoolArena.HeapArena的memoryCopy实现代码:
如何快速更新自己的技术积累?
- 在现有的项目里,深挖技术,比如用到netty可以把相关底层代码和要点都看起来。
- 如果不知道目前的努力方向,就看自己的领导或公司里技术强的人在学什么。
- 知道努力方向后不知道该怎么学,就到处去找相关资料然后练习。
- 学习以后不知道有没有学成,则可以通过面试去检验。
我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目
[外链图片转存中…(img-7E5QWipf-1714430790328)]
[外链图片转存中…(img-Cmp1DqfB-1714430790329)]