内存回收
netty内部是通过计数来实现内存的回收的,具体的实现是在AbstractReferenceCountedByteBuf中。所以我们在使用完pooledBytebuf后,要记得调用release方法。
我们跟踪release方法看到会执行到io.netty.buffer.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();
}
}
这里会调用chunk所属arena的free方法,我们继续跟进
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)) {
// cached so not free it.
return;
}
//如果队列满了或者其他原因导致加入队列失败则标记这个内存未被使用
freeChunk(chunk, handle, sizeClass, nioBuffer, false);
}
}
这里如果是非池化的的直接释放内存
protected void destroyChunk(PoolChunk<ByteBuffer> chunk) {
//通过unsafe释放内存
if (PlatformDependent.useDirectBufferNoCleaner()) {
PlatformDependent.freeDirectNoCleaner(chunk.memory);
} else {
//通过cleaner释放直接内存
PlatformDependent.freeDirectBuffer(chunk.memory);
}
}
直接内存通过两种方式释放堆外内存,一种是使用cleaner执行clean方法释放内存,一种是执行unsafe方法释放内存。如果是堆内内存则可以等待GC释放内存。
如果是池化的内存,判断当时申请时候的类型,然后将这块内存放入PoolThreadCache中等待下次使用。
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);
}
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();
}
}
public final boolean add(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle) {
//从对象池中获取一个entry,并且将所有的chunk、内存对象和handle信息进行赋值
Entry<T> entry = newEntry(chunk, nioBuffer, handle);
//加入队列中
boolean queued = queue.offer(entry);
//如果放入失败将entry回收
if (!queued) {
// If it was not possible to cache the chunk, immediately recycle the entry
entry.recycle();
}
return queued;
}
这里就是将entry放入到线程本地缓存的MemoryRegionCache的队列中,等下次复用
如果队列满了或者其他原因导致加入队列失败则标记这个内存未被使用
void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) {
final boolean destroyChunk;
synchronized (this) {
// We only call this if freeChunk is not called because of the PoolThreadCache finalizer as otherwise this
// may fail due lazy class-loading in for example tomcat.
if (!finalizer) {
switch (sizeClass) {
case Normal:
++deallocationsNormal;
break;
case Small:
++deallocationsSmall;
break;
case Tiny:
++deallocationsTiny;
break;
default:
throw new Error();
}
}
destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer);
}
//如果高峰时期申请了很多内存特别是堆外内存,现在使用率慢慢下来了,当poolChunk使用率为0了,要将内存释放
if (destroyChunk) {
// destroyChunk not need to be called while holding the synchronized lock.
destroyChunk(chunk);
}
}
我们跟踪到free方法
boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) {
chunk.free(handle, nioBuffer);
//如果整个chunk的使用率下降到当前这个chunkList的最小值了就移动这个chunk到符合使用率的chunkList中
if (chunk.usage() < minUsage) {
remove(chunk);
// Move the PoolChunk down the PoolChunkList linked-list.
return move0(chunk);
}
return true;
}
继续跟进
void free(long handle, ByteBuffer nioBuffer) {
int memoryMapIdx = memoryMapIdx(handle);//二叉树中位置
int bitmapIdx = bitmapIdx(handle);//位图信息
//如果是subpage
if (bitmapIdx != 0) { // free a subpage
//找到在PoolSubpage数组中的位置
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
assert subpage != null && subpage.doNotDestroy;
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
//找到tinySubpagePools中的head
PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
synchronized (head) {
//bitmapIdx & 0x3FFFFFFF:表示属于第一个subpage
if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
return;
}
}
}
//如果释放的是page级别的
//修改可用字节数
freeBytes += runLength(memoryMapIdx);
//修改内存管理数组memoryMap的值,设置为没有占用
setValue(memoryMapIdx, depth(memoryMapIdx));
//修改父节点,和分配是一个反向过程
updateParentsFree(memoryMapIdx);
if (nioBuffer != null && cachedNioBuffers != null &&
cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
cachedNioBuffers.offer(nioBuffer);
}
}
继续跟进如果是释放subpage基本的内存,跟踪subpage.free方法
boolean free(PoolSubpage<T> head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}
//在bitmap数组中的索引位置
int q = bitmapIdx >>> 6;
//相对第几个subpage
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) != 0;
//将这个位置设置为0
bitmap[q] ^= 1L << r;
//设置下一个可用位置
setNextAvail(bitmapIdx);
//head指向下一个
if (numAvail ++ == 0) {
addToPool(head);
return true;
}
//表示subpage还在被占用
if (numAvail != maxNumElems) {
return true;
} else {
//如果只剩下一个标记分配大小的head节点直接返回
if (prev == next) {
return true;
}
//pool中有别的poolSubPage
doNotDestroy = false;
//从tinyPoolSubpagePools[]数组上移除
removeFromPool();
return false;
}
}
这里根据bitmapIdx计算在位图数组中的位置,然后将bit为重新设置为0,也就是没有被占用状态,然后设置下一个可用位置就是当前释放的位置,如果可用大小和最大可分配内容不相同则直接返回,说明这个poolSubpage还在被占用,如果相等,查看是不是就一个head节点,如果是也直接返回,如果不是可能是别的poolSubpage加入了pool,则将释放的subpage移除。
下面我们看看如果释放内存是大于一个page的情况
//如果释放的是page级别的
//修改可用字节数
freeBytes += runLength(memoryMapIdx);
//修改内存管理数组memoryMap的值,设置为没有占用
setValue(memoryMapIdx, depth(memoryMapIdx));
//修改父节点,和分配是一个反向过程
updateParentsFree(memoryMapIdx);
if (nioBuffer != null && cachedNioBuffers != null &&
cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
cachedNioBuffers.offer(nioBuffer);
}
首先更新可用内存大小,然后更新内存管理数组memoryMap中的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 {
//如果兄弟节点还被占用,则跟新父节点的值为刚刚释放的id的值
byte val = val1 < val2 ? val1 : val2;//去小的值
setValue(parentId, val);
}
//向上传递,继续跟新这颗树
id = parentId;
}
}
这里其实就是分配过程的反过程,更新内存管理数组memoryMap。我们继续回到PoolChunkList#free方法
boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) {
chunk.free(handle, nioBuffer);
//如果整个chunk的使用率下降到当前这个chunkList的最小值了就移动这个chunk到符合使用率的chunkList中
if (chunk.usage() < minUsage) {
remove(chunk);
return move0(chunk);
}
return true;
}
private boolean move0(PoolChunk<T> chunk) {
//前面已经没有poolChunkList了
if (prevList == null) {
assert chunk.usage() == 0;//使用率为0
return false;
}
return prevList.move(chunk);
}
如果释放后使用率低于最小值了,将poolChunk从当前的poolChunkList移除,移动到前一个中去,如果前边一个list为null了,也就是移动到了q000了,并且使用率为0,则说明这个poolChunk没有任何占用可以移除了。回到
io.netty.buffer.PoolArena#freeChunk方法执行销毁chunk操作。
//如果高峰时期申请了很多内存特别是堆外内存,现在使用率慢慢下来了,并且poolChunk使用率为0了,要将内存释放
if (destroyChunk) {
// destroyChunk not need to be called while holding the synchronized lock.
destroyChunk(chunk);
}
所以我们在使用池化内存的时候切记要执行release操作