2014年9月13日星期六

学习一下netty4的内存管理

        按照内存分配策略划分,netty4的比较典型的ByteBuf有几种,UnpooledHeapByteBuf,UnpooledDirectByteBuf,UnpooledUnsafeDirectByteBuf,PooledHeapByteBuf,PooledDirectByteBuf,PooledUnsafeDirectByteBuf。其中UnpooledUnsafeDirectByteBuf和PooledUnsafeDirectByteBuf的实现比较新颖,所以花时间学习记录一下。

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做更精细的控制。
      PooledByteBufAllocator.newDirectBuffer 的流程大致如下图,

这个流程中有几个要注意的地方,
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 算法的变化

https://github.com/netty/netty/pull/2582
这个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