C++风格的显式内存释放
UnpooledUnsafeDirectByteBuf调用java.nio.ByteBuffer.allocateDirect在native内存区分配内存,和直接使用java.nio.DirectByteBuffer稍有不同的是netty自己实现了buffer的生命周期管理,而不是依赖System.gc()去释放DirectByteBuffer所占用的的native内存。释放内存的方法是PlatformDependent0.freeDirectBuffer,主要是下面两行,
Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
cleaner.clean();
什么时候释放呢?UnpooledUnsafeDirectByteBuf继承了AbstractReferenceCountedByteBuf,而AbstractReferenceCountedByteBuf实现了ReferenceCounted接口。ReferenceCounted的作用是对目标对象进行引用计数,而在AbstractReferenceCountedByteBuf.release() 里面有一段,
if (refCntUpdater.compareAndSet(this, refCnt, refCnt - 1)) {
if (refCnt == 1) {
deallocate();
return true;
}
return false;
}
只有当引用计数减到1的时候才会真正调释放内存的方法。
下面是netty文档里使用引用计数的例子,
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;
buf.retain();
assert buf.refCnt() == 2;
boolean destroyed = buf.release();
assert !destroyed;
assert buf.refCnt() == 1;
netty还有一个ResourceLeakDetector用来检测引用计数是否被正确的使用,
UnpooledByteBufAllocator allocator=new UnpooledByteBufAllocator(true);
while (true){
ByteBuf buf = allocator.directBuffer();
}
运行上面几句就能看到错误信息,LEAK: ByteBuf.release() was not called before it's garbage-collected. ...
netty的资源泄漏检测是通过PhantomReference实现的,UnpooledByteBufAllocator.directBuffer 创建buffer的时候会抽样创建DefaultResourceLeak(extends PhantomReference),而ResourceLeakDetector通过检查DefaultResourceLeak的refQueue来判断DefaultResourceLeak指向的资源有没有被GC回收,如果被GC回收就报告resource leak。感觉是和java GC反过来的,很有意思。引用计数加上资源泄漏检查感觉是一个完整的内存管理方案了。
关于引用计数更详细的信息见官方文档, http://netty.io/wiki/reference-counted-objects.html 。
本地内存池方案
PooledUnsafeDirectByteBuf的实现要更复杂一些。netty内存池的管理方案是 buddy allocation + slab allocation, https://blog.twitter.com/2013/netty-4-at-twitter-reduced-gc-overhead
wikipedia对'buddy allocation'的描述是有一段,'This problem can be solved by slab allocation, which may be layered on top of the more coarse buddy allocator to provide more fine-grained allocation.' 我理解是buddy allocation做比较粗粒度的分配,slab allocation做更精细的控制。
这个流程中有几个要注意的地方,
1. PoolArena.allocate 大部分流程是加锁的,所以ThreadCache很重要。PooledByteBuf.deallocate调用的时候,buf对应的PoolChunk会被放进ThreadCache。
2. PoolArena有一组ChunkList,分别对应内存消耗0%,25%...100%的chunk,分配内存的时候先在各个ChunkList里面分配。
3. buddy allocate的时候需要的内存如果小于page size(默认8K)会分配PoolSubpage,否则就只在PoolChunk里面分配。
buddy allocation
这里先说说4.0.19的实现,后面再看看4.0.21算法的变化。
如上图buddy allocation将chunk中的内存组织成一颗完全二叉树,假设chunk的大小是128k,在这颗树中每一个结点的位置就可以决定这个结点代表的内存大小,在上面这颗树中叶子结点也是最小的内存单位是8k。
netty中chunk的默认值是16m,page的默认值是8k,所以PoolChunk有一个大小为2048的PoolSubpage。
PoolSubpage<T>[] subpages;
而对应的完全二叉树的大小是4096。
int[] memoryMap;
memoryMap里面保存的是当前内存结点的信息,其中0-1位是当前结点的状态(0:未使用,1:拆分,2:已分配,3:已分配的SubPage),2-16位是当前块的大小,17-31位是当前块的地址偏移。
内存分配的过程就是在这颗二叉树中下降寻找大小合适的结点,如果要分配的内存小于8k,最后会调用PoolSubpage.allocate。
一个PoolSubpage仅用于做一个固定elemSize的内存分配。elemSize的值可以是16, 32, 64 ...
这也就是每个page slab的大小。page用一个位图
long[] bitmap;
来描述这个page里面内存分配的情况,内存以16个字节为一个基本单位,所以bitmap数组的大小是pageSize / 16 / 64。
每次内存分配以后会返回一个long型的handle来描述这个page的状态,
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
有了这个handle,这个chunk就可以放进缓存,下次拿到这个chunk的时候再拿到这个handle就知道上次用的是哪个page里面的哪个位置,然后就可以重复使用上次用过那个位置的内存。
slab allocation
PoolThreadCache
tinySubPageCaches[32]
16 32 48 64 ... 512
smallSubPageCaches[4]
1k 2k 4k 8k
normalCaches[4]
8k 16k 32k 64k
ThreadCache 大致的结构如上,netty根据每次申请内存的大小决定在cache的哪个位置尝试分配内存,比如要申请2k的内存会返回smallSubPageCaches[1]对应的MemoryRegionCache,然后在MemoryRegionCache中寻找可用的PoolChunk。
PoolArena里面也有类似的结构(tinySubpagePools,smallSubpagePools)。
4.0.21 算法的变化
这个change 的效果是不用Thread Cache的情况下分配大于8k内存的效率提高了很多。
主要的变化是用
byte[] memoryMap;
byte[] depthMap;
代替了
long[] memoryMap;
其中depthMap纯做索引,用来帮助计算node的容量和偏移量。memoryMap和depthMap初始化的状态都是像下面这样,
[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, ...]
是一个保存当前高度的完全二叉树,
memoryMap[id]对应的值是id结点能分配最大内存的高度(h)表示,
分配内存的时候先将内存大小换算成高度(h),
int d = maxOrder - (log2(normCapacity) - pageShifts);
比如要分配16k字节内存,对应的高度是10,表示在高度10的结点代表的内存大小是16k。
这种情况下如果memoryMap[id] <= 10 就说明id结点上可以分配16k的内存。
分配的时候从根结点往下找,返回的是memoryMap[id] <= d 的结点,而且id在树里的高度要尽量大。
在分配内存以后memoryMap的值会改变,具体就是当前分配的结点memoryMap[id]=unusable,这个unusable是当前树的最大高度+1,id的祖先结点的值可能会+1(取决于祖先结点另一个孩子的值)这个算法比之前主要的提升在于
1. 内存的使用减小。
2. 找可用结点的时候搜索次数减少,从上往下,从左往右,不会回溯。
感觉netty在内存管理方面做的事情还是比较多的,有不少技巧都可以借用到平时的工作中。
简单记录思考一下tcmalloc
1. page内部不是用的bitmap,而是用的可以擦除的链表结构,为了节省内存。page和span就不是可擦除的结构了,page只是个管理对象本身应该不和管理的内存放在一起。
2. 小于page的也是使用slab分配方式。应该也是有一些page专用来做slab分配,猜想有32byte page,64byte page这样的概念。
3. 大于page的内存管理使用span,span合并的时候需要radixTree。这是和buddy方式最大的不同。
4. span本身也是通过slab的方式来索引。
5. 同样有theadcache
没有评论:
发表评论