这个分享阅读的是4.1.52.Final-SNAPSHOT这个版本的源码
netty源码阅读五(内存池之PoolArena和PoolChunkList)
poolChunk表示的内存池中一整块的内存,也是内存池向java虚拟机申请和释放的最小单位,即内存池每次会向虚拟机申请一个PoolChunk内存来进行分配,并在PoolChunk空闲时将PoolChunk中的内存释放。
内存池对于内存的分配其最终分配的是内存所处的PoolChunk以及PoolChunk下的句柄handle。
重要术语
在介绍PoolChunk代码之前先介绍几个比较重要的术语。
- page:page是PoolChunk的分配的最小单位,默认的1个page的大小为1kB
- run:表示的是一个page的集合
- handle:句柄,用于表示poolChunk中一块内存的位置,大小,使用情况等信息,下面的图展示了一个handle的对应的位数的含义。可以看到一个handle其实是一个64为的数据,其前15位表示的是这个句柄所处的位置,即第几页,然后15位表示的是这个句柄表示的是多少页,isUsed表示这一段内存是否被使用,isSubpage表示的这一段内存是否用于subPage的分配,bitmapIdx表示的是这块内存在subPage中bitMap的第几个
源码阅读
PoolChunk的数据结构
下面是PoolChunk主要的数据结构。
final class PoolChunk<T> implements PoolChunkMetric {
...
//所处的PoolArena
final PoolArena<T> arena;
//维护的内存块,用泛型区分是堆内存还是直接内存
final T memory;
//表示这个PoolChunk是没进行池化操作的,主要为Huge的size的内存分配
final boolean unpooled;
final int offset;
//存储的是有用的run中的第一个和最后一个Page的句柄
private final IntObjectMap<Long> runsAvailMap;
//管理所有有用的run,这个数组的索引是SizeClasses中page2PageIdx计算出来的idx
//即sizeClass中每个size一个优先队列进行存储
private final PriorityQueue<Long>[] runsAvail;
//管理这个poolchunk中所有的poolSubPage
private final PoolSubpage<T>[] subpages;
//一个page的大小
private final int pageSize;
//pageSize需要左移多少位
private final int pageShifts;
//这个chunk的大小
private final int chunkSize;
//主要是对PooledByteBuf中频繁创建的ByteBuffer进行缓存,以避免由于频繁创建的对象导致频繁的GC
private final Deque<ByteBuffer> cachedNioBuffers;
//空闲的byte值
int freeBytes;
//所处的PoolChunkList
PoolChunkList<T> parent;
//所处双向链表前一个PoolChunk
PoolChunk<T> prev;
//所处双向两边的后一个PoolChunk
PoolChunk<T> next;
}
上面的数据结构中主要是runsAvailMap,runsAvail,subPages和memory这4块数据,下面图描述了这几块数据具体存储的内容。
其中memory部分绿色表示已经分配了的页,空白的表示还没被分配的页,青色部分表示的被分配为subPage的页。
可以看到runsAvailMap存储的是runOffset->handle之间的键值对,并且其存储的是空闲块的第一页和最后一页的句柄。runsAvail则是维护的一个优先队列数组,其数组的索引其实是size对应的sizeIdx,可以看到空闲页为2页的内存的三块内存的句柄都存在了一个优先队列中,而空闲页为5页的内存则存在另一个对应位置的优先队列中。subPages则数一个PoolSubPage数组,其数组的索引为page的runOffset,存储的是以这个offset开始的PoolSubPage对象
PoolChunk主要的是allocate和free这两个方法,处理分配和释放两个操作,下面来介绍一下这两个方法。
allocate
可以看到这个allocate方法其实就是将subPage的分配委托给allocateSubpage方法,对于不是subPage的分配委托给了allocateRun进行操作。
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache cache) {
final long handle;
//这里的sizeIdx表示的是sizeClass的size2sizeIdx计算的对应的sizeIdx
//这里的smallMaxSizeIdx表示的是最大的subPage的对应的索引
if (sizeIdx <= arena.smallMaxSizeIdx) {
// 这种size则以subPage的形式进行分配
handle = allocateSubpage(sizeIdx);
if (handle < 0) {
return false;
}
assert isSubpage(handle);
} else {
//利用allocateRun分配多个整的page
int runSize = arena.sizeIdx2size(sizeIdx);
handle = allocateRun(runSize);
if (handle < 0) {
return false;
}
}
//从cachedNioBuffers获取缓存的ByteBuffer,一个PoolChunk下的所有的这些ByteBuffer
//其实指向的都是同一块区域,即memory
ByteBuffer nioBuffer = cachedNioBuffers != null? cachedNioBuffers.pollLast() : null;
//初始化
initBuf(buf, nioBuffer, handle, reqCapacity, cache);
return true;
}
allocateRun
allocateRun是分配多个page操作,其主要操作是从runAvail中找到最接近当前需要分配的size的内存块,然后将其进行切分出需要分配的内存块,并将剩下的空闲块再存到runAvail和runsAvailMap中。
private long allocateRun(int runSize) {
int pages = runSize >> pageShifts;
//根据page的数量计算对应的pageIdx
//因为runAvail这个数组用的则是这个idx为索引的
int pageIdx = arena.pages2pageIdx(pages);
//这里的runsAvail保证的是runsAvail和runsAvailMap数据的同步
synchronized (runsAvail) {
//从当前的pageIdx从ranAvail中找到最近的能进行此次分配idx索引
int queueIdx = runFirstBestFit(pageIdx);
if (queueIdx == -1) {
return -1;
}
//从这个索引中获取对应的run的数据,即为能进行此次分配的空闲段
PriorityQueue<Long> queue = runsAvail[queueIdx];
long handle = queue.poll();
assert !isUsed(handle);
//从queue和runsAvailMap中移除这个handle
removeAvailRun(queue, handle);
if (handle != -1) {
//将这一块内存进行切分,剩余空闲的内存继续存储到ranAvail和runsAvailMap中
handle = splitLargeRun(handle, pages);
}
//更新freeBytes
freeBytes -= runSize(pageShifts, handle);
return handle;
}
}
下面图描述了对上面那幅图进行了一次3个page的分配操作后对应的内存的数据结构,其中红色表示的是这次分配的内存,可以看到原来在5kB的内存数据到2kB中,并且存储在runsAvaliMap中的对应的key也由原来的12移到了现在的15
allocateSubpage
这个方法是分配一个subPage,其主要的逻辑是先对sizeIdx这个索引在sizeClasses中所对应的size为基础的elemSize获取一个这个elemSize和pageSize的最小公倍数大小的内存,将这块内存分为大小相等的以elmSize的subPage利用PoolSubPage来进行维护。
private long allocateSubpage(int sizeIdx) {
//根据sizeIdx计算出其对应的PoolSubpage,arena以sizeIdx为key存储了一个散列表
//来存储PoolSubpage,其每个链表的头都是一个特殊的不做内存分配的PoolSubpage的head
//对于其对应链表的操作都需要对这个链表的head加锁
PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
synchronized (head) {
//计算第一个对这个sizeIdx对应的size与pageSize的最小公倍数
int runSize = calculateRunSize(sizeIdx);
//获取这个runSize的对应打下的内存
long runHandle = allocateRun(runSize);
if (runHandle < 0) {
return -1;
}
int runOffset = runOffset(runHandle);
int elemSize = arena.sizeIdx2size(sizeIdx);
//把这个分配的runSize切分为runSize/elemSize个相同的elemSize大小的subPage
//利用PoolSubpage进行分配
PoolSubpage<T> subpage = new PoolSubpage<T>(head, this, pageShifts, runOffset,
runSize(pageShifts, runHandle), elemSize);
//将这个subPage存在subpages中
subpages[runOffset] = subpage;
return subpage.allocate();
}
}
free
free的进行释放操作,主要操作如果是subpage,利用PoolSubpage进行释放。对于多页的释放则会利用runsAvailMap合并其前后的空闲的内存块,因为runsAvailMap中存储了空闲内存块的头和尾,所以对内存块的合并很简单,即为以当前的头和尾的前一个或者后一个为key能否找到对应的空闲内存合并即可。
void free(long handle, int normCapacity, ByteBuffer nioBuffer) {
//释放的是subPage
if (isSubpage(handle)) {
int sizeIdx = arena.size2SizeIdx(normCapacity);
PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
PoolSubpage<T> subpage = subpages[runOffset(handle)];
assert subpage != null && subpage.doNotDestroy;
synchronized (head) {
//PoolSubPage释放这块内存,返回true则表示这块PoolSubPage还在用
if (subpage.free(head, bitmapIdx(handle))) {
//the subpage is still used, do not free it
return;
}
}
}
//start free run
int pages = runPages(handle);
synchronized (runsAvail) {
//与这块内存前后相邻的内存空闲内存进行合并
long finalRun = collapseRuns(handle);
//将IS_USED和IS_SUBPAGE标志位设置为0
finalRun &= ~(1L << IS_USED_SHIFT);
//if it is a subpage, set it to run
finalRun &= ~(1L << IS_SUBPAGE_SHIFT);
//将合并后的句柄存储到runAvail和runsAvailMap中
insertAvailRun(runOffset(finalRun), runPages(finalRun), finalRun);
freeBytes += pages << pageShifts;
}
//将这个ByteBuf创建的ByteBuffer存到cachedNioBuffers缓存中
if (nioBuffer != null && cachedNioBuffers != null &&
cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
cachedNioBuffers.offer(nioBuffer);
}
}
下面图则是对开始的内存数据将offset为8的内存块释放后其内存数据结构的情况,可以看到其将前面和后面空闲的内存块合并了,成为了5页的内存块,其对应的runsAvailMap和runsAvail中的数据也进行了相应的改变。