精华内容
下载资源
问答
  • 真实内存指的是ByteBuf底层的array和DirectByteBuffer,真实内存池化的好处如下: 降低真实内存的分配开销。因为DirectByteBuffer的分配很抵效(创建堆外内存的速度比堆内存慢了10到20倍),特别的对于...

    真实内存指的是ByteBuf底层的array和DirectByteBuffer,真实内存池化的好处如下:

    • 降低真实内存的分配开销。因为DirectByteBuffer的分配很抵效(创建堆外内存的速度比堆内存慢了10到20倍),特别的对于DirectByteBuffer的分配开销降低尤为明显。
    • 因为真实内存可以复用,避免了大量对象的回收,降低了GC频率。

    怎么降低线程竞争的

    • 默认有核数*2个PoolArea,每个线程初始绑定一个最少线程使用的PoolArea,线程一旦绑定了对应的PoolArea,不能再改变PoolArea。
    • 本地线程也有cache。

    1. 总体结构

    1.1 整体结构

    首先介绍些netty内存池的层级结构,主要分为Arena、ChunkList、Chunk、Page、Subpage这5个层级,这几个层级的关系由大到小,如下图所示:

    Arena代表1个内存区域,为了优化内存区域的并发访问,netty中内存池是由多个Arena组成的数组,分配时会每个线程按照轮询策略选择1个Arena进行内存分配。

    1个Arena由两个PoolSubpage数组和多个ChunkList组成。两个PoolSubpage数组分别为tinySubpagePools和smallSubpagePools。多个ChunkList按照双向链表排列,每个ChunkList里包含多个Chunk,每个Chunk里包含多个Page(默认2048个),每个Page(默认大小为8k字节)由多个Subpage组成。

    每个Arena由如下几个ChunkList构成:

    • PoolChunkList<T> qInit:存储内存利用率0-25%的chunk
    • PoolChunkList<T> q000:存储内存利用率1-50%的chunk
    • PoolChunkList<T> q025:存储内存利用率25-75%的chunk
    • PoolChunkList<T> q050:存储内存利用率50-100%的chunk
    • PoolChunkList<T> q075:存储内存利用率75-100%的chunk
    • PoolChunkList<T> q100:存储内存利用率100%的chunk

    每个ChunkList里包含的Chunk数量会动态变化,比如当该chunk的内存利用率变化时会向其它ChunkList里移动。

    每个Chunk里默认包含2048个Page。

    每个Page包含的Subpage的大小和个数由首次从该Page分配的内存大小决定,1个page默认大小为8k,如果首次在该page中需要分配1k字节,那么该page就被分为8个Subpage,每个Subpage大小为1k。

    1.2 PoolArea申请流程

    在PoolArea中申请内存的流程图如下:

    • 对于小于pageSize大小的内存,会在tinySubpagePools或smallSubpagePools中分配,tinySubpagePools用于分配小于512字节的内存,smallSubpagePools用于分配大于512小于pageSize的内存。
    • 对于大于pageSize小于chunkSize大小的内存,会在PoolChunkList的Chunk中分配。
    • 对于大于chunkSize大小的内存,直接创建非池化Chunk来分配内存,并且该Chunk不会放在内存池中重用。

    1.3 PoolChunkList申请流程

    对于在q050、q025、q000、qInit、q075这些PoolChunkList里申请内存的流程图如下:

    • 在PoolChunk中,数组组织呈完美二叉树数据结构。二叉树叶子节点为2048个Page,每个Page的父节点用于分配pageSize*2大小内存,同理,对于Page叶子节点的父节点的父节点,用于分配pageSize*4大小的内存,后面以此类推。
    • 在初始状态时,tinySubpagePools和smallSubpagePools为空,因此最初分配小于pageSize的内存时,需要新建1个PoolChunk来分配这块小内存,PoolChunk会对Page分类成若干Subpage,然后用Subpage分配这块小内存,最后会把该Subpage放在tinySubpagePools或smallSubpagePools中。

    2. 具体细节

    2.1 PoolChunk

    Netty一次向系统申请16M的连续内存空间,这块内存通过PoolChunk对象包装,为了更细粒度的管理它,进一步的把这16M内存分成了2048个页(pageSize=8k)。页作为Netty内存管理的最基本的单位 ,所有的内存分配首先必须申请一块空闲页。(Ps: 这里可能有一个疑问,如果申请1Byte的空间就分配一个页是不是太浪费空间,在Netty中Page还会被细化用于专门处理小于4096Byte的空间申请 )那么这些Page需要通过某种数据结构跟算法管理起来。最简单的是采用数组或位图管理

    如上图1表示已申请,0表示空闲。这样申请一个Page的复杂度为O(n),但是申请k个连续Page,就立马退化为O(kn)。

    Netty采用完全二叉树进行管理,树中每个叶子节点表示一个Page,即树高为12,中间节点表示页节点的持有者。

    这样的一个完全二叉树可以用大小为4096的数组表示,数组元素的值含义为:

    private final byte[] memoryMap; //表示完全二叉树,共有4096个
    private final byte[] depthMap; //表示节点的层高,共有4096个
    
    1. memoryMap[i] = depthMap[i]:表示该节点下面的所有叶子节点都可用,这是初始状态
    2. memoryMap[i] = depthMap[i] + 1:表示该节点下面有一部分叶子节点被使用,但还有一部分叶子节点可用
    3. memoryMap[i] = maxOrder + 1 = 12:表示该节点下面的所有叶子节点不可用

    有了上面的数据结构,那么页的申请跟释放就非常简单了,只需要从根节点一路遍历找到可用的节点即可,复杂度为O(lgn)。代码为:

    #PoolChunk
      //根据申请空间大小,选择申请方法
      long allocate(int normCapacity) {
            if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
                return allocateRun(normCapacity); //大于1页
            } else {
                return allocateSubpage(normCapacity);
            }
        }
      //按页申请
      private long allocateRun(int normCapacity) {
            //计算需要在哪一层开始
            int d = maxOrder - (log2(normCapacity) - pageShifts);
            int id = allocateNode(d); 
            if (id < 0) {
                return id;
            }
            freeBytes -= runLength(id);
            return 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;
        }
      //级联更新父节点的状态信息   
      private void updateParentsAlloc(int id) {
            while (id > 1) {
                int parentId = id >>> 1;
                byte val1 = value(id);
                byte val2 = value(id ^ 1);
                byte val = val1 < val2 ? val1 : val2;
                setValue(parentId, val);
                id = parentId;
            }
        }
    

    2.2 PoolSubpage

    对于小内存(小于4096)的分配还会将Page细化成更小的单位Subpage。Subpage按大小分有两大类,36种情况:

    1. Tiny:小于512的情况,最小空间为16,对齐大小为16,区间为[16,512),所以共有32种情况。
    2. Small:大于等于512的情况,总共有四种,512,1024,2048,4096。

    PoolSubpage中直接采用位图管理空闲空间(因为不存在申请k个连续的空间),所以申请释放非常简单。
    代码:
    #PoolSubpage(数据结构)
        final PoolChunk<T> chunk;   //对应的chunk
        private final int memoryMapIdx; //chunk中那一页,肯定大于等于2048
        private final int pageSize; //页大小
        private final long[] bitmap; //位图
        int elemSize; //单位大小
        private int maxNumElems; //总共有多少个单位
        private int bitmapLength; //位图大小,maxNumElems >>> 6,一个long有64bit
        private int nextAvail; //下一个可用的单位
        private int numAvail; //还有多少个可用单位;
    

    这里bitmap是个位图,0表示可用,1表示不可用. nextAvail表示下一个可用单位的位图索引,初始状态为0,申请之后设置为-1. 只有在free后再次设置为可用的单元索引。在PoolSubpage整个空间申请的逻辑就是在找这个单元索引,只要理解了bitmap数组是个位图,每个数组元素表示64个单元代码的逻辑就比较清晰了

    #PoolSubpage
      long allocate() {
            if (elemSize == 0) {
                return toHandle(0);
            }
    
            if (numAvail == 0 || !doNotDestroy) {
                return -1;
            }
    
            final int bitmapIdx = getNextAvail(); //查找下一个单元索引
            int q = bitmapIdx >>> 6; //转为位图数组索引
            int r = bitmapIdx & 63; //保留最低的8位
            assert (bitmap[q] >>> r & 1) == 0;
            bitmap[q] |= 1L << r; //设置为1
    
            if (-- numAvail == 0) {
                removeFromPool();
            }
    
            return toHandle(bitmapIdx); //对索引进行特化处理,防止与页索引冲突
        }
    
      private int getNextAvail() {
            int nextAvail = this.nextAvail;
            if (nextAvail >= 0) { //大于等于0直接可用
                this.nextAvail = -1;
                return nextAvail;
            }
            return findNextAvail(); //通常走一步逻辑,只有第一次跟free后nextAvail才可用
        }
      //找到位图数组可用单元,是一个long类型,有[1,64]单元可用
      private int findNextAvail() {
            final long[] bitmap = this.bitmap;
            final int bitmapLength = this.bitmapLength;
            for (int i = 0; i < bitmapLength; i ++) {
                long bits = bitmap[i];
                if (~bits != 0) {
                    return findNextAvail0(i, bits);
                }
            }
            return -1;
        }
       //在64的bit中找到一个可用的
        private int findNextAvail0(int i, long bits) {
            final int maxNumElems = this.maxNumElems;
            final int baseVal = i << 6;
    
            for (int j = 0; j < 64; j ++) {
                if ((bits & 1) == 0) {
                    int val = baseVal | j;
                    if (val < maxNumElems) {
                        return val;
                    } else {
                        break;
                    }
                }
                bits >>>= 1;
            }
            return -1;
        }
    

    2.3 PoolSubpage池

    第一次申请小内存空间的时候,需要先申请一个空闲页,然后将该页转成PoolSubpage,再将该页设为已被占用,最后再把这个PoolSubpage存到PoolSubpage池中。这样下次就不需要再去申请空闲页了,直接去池中找就好了。Netty中有36种PoolSubpage,所以用36个PoolSubpage链表表示PoolSubpage池。

    #PoolArena
    private final PoolSubpage<T>[] tinySubpagePools;
    private final PoolSubpage<T>[] smallSubpagePools;
    
    #PoolSubpage
    PoolSubpage<T> prev;
    PoolSubpage<T> next;
    

    #PoolArena
    allocate(...reqCapacity...){
       final int normCapacity = normalizeCapacity(reqCapacity);
       //找到池的类型跟下标
       boolean tiny = isTiny(normCapacity);
       if (tiny) { // < 512
           tableIdx = tinyIdx(normCapacity);
           table = tinySubpagePools;
        } else {
           tableIdx = smallIdx(normCapacity);
           table = smallSubpagePools;
        }
        final PoolSubpage<T> head = table[tableIdx];
        synchronized (head) {
            final PoolSubpage<T> s = head.next;
            if (s != head) {
                //通过PoolSubpage申请
                long handle = s.allocate();
                ...
            }
        }
    }
    

    2.4 PoolChunkList

    上面讨论了PoolChunk的内存分配算法,但是PoolChunk只有16M,这远远不够用,所以会很很多很多PoolChunk,这些PoolChunk组成一个链表,然后用PoolChunkList持有这个链表

    #PoolChunkList
    private PoolChunk<T> head;
    
    #PoolChunk
    PoolChunk<T> prev;
    PoolChunk<T> next;
    

    这里还没这么简单,它有6个PoolChunkList,所以将PoolChunk按内存使用率分类组成6个PoolChunkList,同时每个PoolChunkList还把各自串起来,形成一个PoolChunkList链表。

    #PoolChunkList
     private final int minUsage; //最小使用率
     private final int maxUsage; //最大使用率
     private final int maxCapacity;
    
    private PoolChunkList<T> prevList;
    private final PoolChunkList<T> nextList;
    
    #PoolArena
    //[100,) 每个PoolChunk使用率100%
    q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
    //[75,100) 每个PoolChunk使用率75-100%
    q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
    //[50,100)
    q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
    //[25,75)
    q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
    //[1,50)
    q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
    qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);
    

    既然按使用率分配,那么PoolChunk在使用过程中是会动态变化的,所以PoolChunk会在不同PoolChunkList中变化。同时申请空间,使用哪一个PoolChunkList也是有先后顺序的

    #PoolChunkList
      boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
            if (head == null || normCapacity > maxCapacity) {
                return false;
            }
            for (PoolChunk<T> cur = head;;) {
                long handle = cur.allocate(normCapacity);
                if (handle < 0) {
                    cur = cur.next;
                    if (cur == null) {
                        return false;
                    }
                } else {
                    cur.initBuf(buf, handle, reqCapacity);
                    if (cur.usage() >= maxUsage) {
                        remove(cur); 
                        nextList.add(cur); //移到下一个PoolChunkList中
                    }
                    return true;
                }
            }
        }
    
    #PoolArena
    allocateNormal(...){
      if (q050.allocate(...) || q025.allocate(...) ||
                q000.allocate(...) || qInit.allocate(...) ||
                q075.allocate(...)) {
                return;
            }
      PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
      ...
      qInit.add(c);
    }
    

    这样设计的目的是考虑到随着内存的申请与释放,PoolChunk的内存碎片也会相应的升高,使用率越高的PoolChunk其申请一块连续空间的失败的概率也会大大的提高。

    1. qInit前置节点为自己,且minUsage=Integer.MIN_VALUE,意味着一个初分配的chunk,在最开始的内存分配过程中(内存使用率<25%),即使完全释放也不会被回收,会始终保留在内存中。
    2. q000没有前置节点,当一个chunk进入到q000列表,如果其内存被完全释放,则不再保留在内存中,其分配的内存被完全回收。
    3. 各个PoolChunkList的区间是交叉的,这是故意的,因为如果介于一个临界值的话,PoolChunk会在前后PoolChunkList不停的来回移动。
    4. 为什么area中ChunkList的分配顺序如下面代码所示,要从q050开始?
      4.1 为什么不从q0000开始?
      因为永远从空闲率很高的chunk分配,那么chunk的空闲率就不太可能降为0,chunk自然不会被回收,造成内存得到释放。当负载很大申请了很多的chunk,但是负载降低时chunk又不能及时回收。
      4.2 为什么不从qinit开始?
      因为qinit得不到释放。(为什么?????)
      4.3 为什么不从q075和q100开始?
      q075和q100由于内存利用率太高,导致内存分配的成功率大大降低,因此放到最后;
    if (q050.allocate(...) || q025.allocate(...) ||
                q000.allocate(...) || qInit.allocate(...) ||
                q075.allocate(...)) {
                return;
            }
     

    2.5 PoolArena

    PoolArena是上述功能的门面,通过PoolArena提供接口供上层使用,屏蔽底层实现细节。为了减少线程成间的竞争,很自然会提供多个PoolArena。Netty默认会生成2×CPU个PoolArena跟IO线程数一致。然后第一次使用的时候会找一个使用线程最少的PoolArena

         private <T> PoolArena<T> leastUsedArena(PoolArena<T>[] arenas) {
                if (arenas == null || arenas.length == 0) {
                    return null;
                }
    
                PoolArena<T> minArena = arenas[0];
                for (int i = 1; i < arenas.length; i++) {
                    PoolArena<T> arena = arenas[i];
                    if (arena.numThreadCaches.get() < minArena.numThreadCaches.get()) {
                        minArena = arena;
                    }
                }
    
                return minArena;
            }
    

    2.6 本地线程存储

    虽然提供了多个PoolArena减少线程间的竞争,但是难免还是会存在锁竞争,所以需要利用ThreaLocal进一步优化,把已申请的内存放入到ThreaLocal自然就没有竞争了。大体思路是在ThreadLocal里面放一个PoolThreadCache对象,然后释放的内存都放入到PoolThreadCache里面,下次申请先从PoolThreadCache获取。

    但是,如果thread1申请了一块内存,然后传到thread2在线程释放,这个Netty在内存holder对象里面会引用PoolThreadCache,所以还是会释放到thread1里

    3. 性能测试

    可以写两个简单的测试用例,感受一下Netty内存池带来的效果。

    1. 申请10000000个HeapBuffer,DirectBuffer,池化的DirectBuffer花的时间, 可以看出池化效果非常明显,而且十分平和
    capacity HeapBuffer DirectBuffer 池化的DirectBuffer
    64Byte 465 13211 2059
    256Byte 946 15074 2309
    512Byte 2528 19516 2188
    1024Byte 4393 21928 2044
    1. 启一个DiscardServer,然后发送80G的数据,看下GC次数,效果感人
    非池化 池化 池化+COMPOSITE_CUMULATOR
    208 27 0

     

    展开全文
  • 无论是池化的ByteBuf还是非池化的ByteBuf,BuyeBuf(不一定是该对象)对象在被gc回收之后,通过判断refCount是否为0来判断是否发生了内存泄漏。 netty支持下面四种级别,使用-Dio.netty.leakDetectionLevel=...

    内存检测原理使用的是PhantomReference技术,通过判断ByteBuf的refCount是否为0,判断是否存在内存泄漏。

    1. 检测原理

    无论是池化的ByteBuf还是非池化的ByteBuf,BuyeBuf(不一定是该对象)对象在被gc回收之后,通过判断refCount是否为0来判断是否发生了内存泄漏。

    netty支持下面四种级别,使用-Dio.netty.leakDetectionLevel=advanced可以调节等级。

    • 禁用(DISABLED) - 完全禁止泄露检测,省点消耗。
    • 简单(SIMPLE) - 默认等级,告诉我们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了。
    • 高级(ADVANCED) - 告诉我们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(创建的地方与访问路径一致)只打印一次。对性能有影响。
    • 偏执(PARANOID) - 跟高级选项类似,但此选项检测所有ByteBuf,而不仅仅是取样的那1%。对性能有绝大的影响。

    SIMPLE级别113才采样一个,但是每次新建一个buffer都会执行一次reportLeak进行内存泄漏检测。

    1.1 检测出内存泄漏就说明有问题

    对于非池化的ByteBuf不会有问题,但是池化的ByteBuf有问题。

    1. 因为非池化的ByteBuf即使refCount没减为0,但是gc机制也会回收掉对应内存,所以不会造成大问题。但是如果程序及时释放不使用的内存肯定更好,以便该内存能被及时的会回收掉。
    2. 虽然ByteBuf可以被gc回收,但是ByteBuf内部引用的数组内存还是被池数据结构引用不能被释放,造成内存不能释放给到池子,最终造成池子里无内存可分配。

    1.2 如何释放ByteBuf

    ReferenceCountUtil.release(msg);

    1.3 内存泄漏案例

    设备上行的消息大小超过限制后就直接关闭了channel,而没有释放内存。OOM信息?内存检测信息?

    1.4 程序一定要显示的释放内存

    以非池化的BuyeBuf举例。对于堆内内存,netty会把数组对象置为空使内存可以快速的被GC会收掉。对于直接内存,ntty会直接通过cleaner释放掉内存,而不用等GC机制释放掉(GC释放直接内存不是很靠谱)。

    2. 如何规避内存泄漏

    2.1 为什么要有引用计数器

    Netty里四种主力的ByteBuf,

    • UnpooledHeapByteBuf 底下的byte[]能够依赖JVM GC自然回收;
    • UnpooledDirectByteBuf底下是DirectByteBuffer,如Java堆外内存扫盲贴所述,除了等JVM GC,最好也能主动进行回收;
    • PooledHeapByteBuf 和 PooledDirectByteBuf,则必须要主动将用完的byte[]/ByteBuffer放回池里,否则内存就要爆掉。

    所以,Netty ByteBuf需要在JVM的GC机制之外,有自己的引用计数器和回收过程。

    一下又回到了C的冰冷时代,自己malloc对象要自己free。 但和C时代又不完全一样,内有引用计数器,外有JVM的GC,情况更为复杂。

    2.2 引用计数器常识

    • 计数器基于 AtomicIntegerFieldUpdater,为什么不直接用AtomicInteger?因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量。
    • 所有ByteBuf的引用计数器初始值为1。
    • 调用release(),将计数器减1,等于零时, deallocate()被调用,各种回收。
    • 调用retain(),将计数器加1,即使ByteBuf在别的地方被人release()了,在本Class没喊cut之前,不要把它释放掉。
    • 由duplicate(), slice()和order()所衍生的ByteBuf,与原对象共享底下的buffer,也共享引用计数器,所以它们经常需要调用retain()来显示自己的存在。
    • 当引用计数器为0,底下的buffer已被回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常。

    2.3 谁来负责Release

    在C时代,我们喜欢让malloc和free成对出现,而在Netty里,因为Handler链的存在,ByteBuf经常要传递到下一个Hanlder去而不复还,所以规则变成了谁是最后使用者,谁负责释放。

    另外,更要注意的是各种异常情况,ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。

    2.3.1 InBound Message

    在AbstractNioByteChannel.NioByteUnsafe.read() 处创建了ByteBuf并调用 pipeline.fireChannelRead(byteBuf) 送入Handler链。

    根据上面的谁最后谁负责原则,每个Handler对消息可能有三种处理方式

    • 对原消息不做处理,调用 ctx.fireChannelRead(msg)把原消息往下传,那不用做什么释放。
    • 将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉。
    • 如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,那更要把原消息release掉。

    假设每一个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,所以Netty在Handler链的最末补了一个TailHandler,如果此时消息仍然是ReferenceCounted类型就会被release掉。

    2.3.2 OutBound Message

    要发送的消息由应用所创建,并调用 ctx.writeAndFlush(msg) 进入Handler链。在每个Handler中的处理类似InBound Message,最后消息会来到HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉

    2.3.3 异常发生时的释放

    多层的异常处理机制,有些异常处理的地方不一定准确知道ByteBuf之前释放了没有,可以在释放前加上引用计数大于0的判断避免释放失败;

    有时候不清楚ByteBuf被引用了多少次,但又必须在此进行彻底的释放,可以循环调用release()直到返回true。

    2.4 内存泄漏检测

    所谓内存泄漏,主要是针对池化的ByteBuf。ByteBuf对象被JVM GC掉之前,没有调用release()把底下的DirectByteBuffer或byte[]归还到池里,会导致池越来越大。而非池化的ByteBuf,即使像DirectByteBuf那样可能会用到System.gc(),但终归会被release掉的,不会出大事。

    Netty担心大家不小心就搞出个大新闻来,因此提供了内存泄漏的监测机制。

    Netty默认会从分配的ByteBuf里抽样出大约1%的来进行跟踪。如果泄漏,会有如下语句打印:

     

    LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

    这句话报告有泄漏的发生,提示你用-D参数,把防漏等级从默认的simple升到advanced,就能具体看到被泄漏的ByteBuf被创建和访问的地方。

    • 禁用(DISABLED) - 完全禁止泄露检测,省点消耗。
    • 简单(SIMPLE) - 默认等级,告诉我们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了。
    • 高级(ADVANCED) - 告诉我们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(创建的地方与访问路径一致)只打印一次。对性能有影响。
    • 偏执(PARANOID) - 跟高级选项类似,但此选项检测所有ByteBuf,而不仅仅是取样的那1%。对性能有绝大的影响。

    实现细节

    每当各种ByteBufAllocator 创建ByteBuf时,都会问问是否需要采样,Simple和Advanced级别下,就是以113这个素数来取模(害我看文档的时候还在瞎担心,1%,万一泄漏的地方有所规律,刚好躲过了100这个数字呢,比如都是3倍数的),命中了就创建一个Java堆外内存扫盲贴里说的PhantomReference。然后创建一个Wrapper,包住ByteBuf和Reference。

    simple级别下,wrapper只在执行release()时调用Reference.clear(),Advanced级别下则会记录每一个创建和访问的动作。

    当GC发生,还没有被clear()的Reference就会被JVM放入到之前设定的ReferenceQueue里。

    在每次创建PhantomReference时,都会顺便看看有没有因为忘记执行release()把Reference给clear掉,在GC时被放进了ReferenceQueue的对象,有则以 "io.netty.util.ResourceLeakDetector”为logger name,写出前面例子里的Error级别的日日志。顺便说一句,Netty能自动匹配日志框架,先找Slf4j,再找Log4j,最后找JDK logger。

    值得说三遍的事

    一定要盯紧log里有没有出现 "LEAK: "字样,因为simple级别下它只会出现一次,所以不要依赖自己的眼睛,要依赖grep。如果出现了,而且你用的是PooledBuf,那一定是问题,不要有任何的侥幸,立刻用"-Dio.netty.leakDetectionLevel=advanced" 再跑一次,看清楚它创建和访问的地方。

    功能测试时,最好开着"-Dio.netty.leakDetectionLevel=paranoid"。

    但是,怎么测试都可能存在没有覆盖到的分支。如果内存尚够,可以适当把-XX:MaxDirectMemorySize 调大,反正只是max,平时也不会真用了你的。然后监控其使用量,及时报警。

    展开全文
  • 对于DirectByteBuffer的分配和释放是比较低效的,使用池化技术能快速分配内存池化技术使对象可以复用,从而降低gc频率。 ByteBuf实际包含两部分内容,一部分是底层的真实内存(array或者DirectByteBuffer)和...

    Netty的ByteBuf分为池化的和非池化的,池化的优点包含如下两点:

    1. 对于DirectByteBuffer的分配和释放是比较低效的,使用池化技术能快速分配内存。
    2. 池化技术使对象可以复用,从而降低gc频率。

    ByteBuf实际包含两部分内容,一部分是底层的真实内存(array或者DirectByteBuffer)和ByteBuf对象。真实内存的池化参见本文第2部分,ByteBuf的对象的池化参见本文第1部分。Netty的ByteBuf泄漏检测(池化和非池化都能检测)原理参见本文的第3部分。

    内存池是一套比较成熟的技术了,Netty的内存池方案借鉴了jemalloc。了解一下其背后的实现原理对阅读Netty内存池的源代码还是很有帮助的

    1. jemalloc原理
    2. glibc内存管理ptmalloc源代码分析

    为了不陷入无尽的细节泥沼之中,应该先了解下jemalloc的原理,然后就可以构想出内存池大概思路:

    1. 首先应该会向系统申请一大块内存
    2. 然后通过某种算法管理这块内存并提供接口让上层申请空闲内存
    3. 申请到的内存地址应该透出到应用层,但是对开发人员来说应该是透明的,所以要有一个对象包装这个地址,并且这个对象应该也是池化的,也就是说不仅要有内存池,还要有一个对象池。

    所以,自然可以带着以下问题去看源码:

    • 内存池管理算法是怎么做到申请效率,怎么减少内存碎片
      主要在于数据结构的组织。Chunk、smallSubPage、tinySubPage、ChunkList按照使用率分配、Chunk的二叉树内存管理、subPage的位图内存管理。
    • 高负载下内存池不断扩展,如何做到内存回收
    • 对象池是如何实现的,这个不是关键路径,可以当成黑盒处理
    • 内存池跟对象池作为全局数据,在多线程环境下如何减少锁竞争
    • 池化后内存的申请跟释放必然是成对出现的,那么如何做内存泄漏检测,特别是跨线程之间的申请跟释放是如何处理的。
      因为采取线程级cache机制,涉及到线程结束时对象的释放,其本质机制是虚引用。从ThreadLocal、WeakHashMap(Map<Stack,WeakOrderQueue>)都是虚引用的用法
    展开全文
  • 在netty的池化ByteBuf分配中,包含ByteBuf对象的池化和真实内存(array或者DirectByteBuffer)的池化。 实际上Recycler不仅可以用于ByteBuf对象的池化,他是一个通用的对象池化技术,我们可以直接使用Recycler实现...

    在netty的池化ByteBuf分配中,包含ByteBuf对象的池化和真实内存(array或者DirectByteBuffer)的池化。

    实际上Recycler不仅可以用于ByteBuf对象的池化,他是一个通用的对象池化技术,我们可以直接使用Recycler实现自身系统对象的池化。

    1. 对象池化的作用

    在netty中ByteBuf的创建是个非常频繁的过程,使用对象池可以起到对象可以复用,减少gc频率的作用。

    2. Recycler的关键技术点

    2.1 多线程竞争下的对象分配和释放(怎么减小多线程竞争)

    每个线程都有自己的对象池,分配时从自己的对象池中获得一个对象。其他线程release对象时,把对象归还到原来自己的池子中去(分配线程的池子)。

    大量使用了ThreadLocal,每个线程都有自己的stack和weakorderqueue,做到线程封闭,有力减小竞争。

    2.2 对象的分配

    1. 先从stack中分配,如果stack有,则直接stack.pop获得对象
    2. 如果stack中没有,从WeakOrderQueue中一次移取多个对象到stack中(每次会尽可能scavenge整个head Link到stack中),最后stack.pop获得对象。

    2.3 对象的release

    1. 如果release线程就是对象的分配线程,直接入栈
    2. 如果release线程和对象的分配线程不是同一个线程,则归还到分配线程的WeakOrderQueue中。release线程维护的数据结构为:ThreadLocal<Map<Stack,WeakOrderQueue>

    3. 未解疑问

    为什么需要WeakOrderQueue?

    WeakOrderQueue的存在是用于非分配对象的线程在release对象时将对象归还到分配线程的对象池中,如果直接把对象归还到release对象的线程中,也就是release线程直接stack.push岂不是很高效?

    INITIAL_CAPACITY = min(DEFAULT_MAX_CAPACITY_PER_THREAD, 256);
    maxCapacity=32768

    4. Recycler核心结构

    Recycler关联了4个核心类:

    1. DefaultHandle:对象的包装类,在Recycler中缓存的对象都会包装成DefaultHandle类。
    2. Stack:存储本线程回收的对象。对象的获取和回收对应Stack的pop和push,即获取对象时从Stack中pop出1个DefaultHandle,回收对象时将对象包装成DefaultHandle push到Stack中。Stack会与线程绑定,即每个用到Recycler的线程都会拥有1个Stack,在该线程中获取对象都是在该线程的Stack中pop出一个可用对象。
      stack底层以数组作为存储结构,初始大小为256,最大大小为32768。
    3. WeakOrderQueue:存储其它线程回收到分配线程的对象,当某个线程从Stack中获取不到对象时会从WeakOrderQueue中获取对象。
      3.1 从分配线程的角度来看,每个分配线程的Stack拥有1个WeakOrderQueue链表,每个WeakOrderQueue元素维持了对应release线程归还的对象。每个分配线程的WeakOrderQueue链表的对象池子中的对象数量不能超过availableSharedCapacity = new AtomicInteger(max(maxCapacity / maxSharedCapacityFactor, LINK_CAPACITY));,该值默认为16K=16384。
      3.2 从release角度看,主要的数据结构为:

      Map<Stack<?>, WeakOrderQueue>维持了release线程向每个分配线程归还对象的数据结构,Map的大小不超过MAX_DELAYED_QUEUES_PER_THREAD = max(0, SystemPropertyUtil.getInt("io.netty.recycler.maxDelayedQueuesPerThread", // We use the same value as default EventLoop number NettyRuntime.availableProcessors() * 2));

      也就是release线程默认最多能向核数*2个分配线程归还对象。
    4. Link: WeakOrderQueue中包含1个Link链表,回收对象存储在链表某个Link节点里,当Link节点存储的回收对象满了时会新建1个Link放在Link链表尾。
      private static final class Link extends AtomicInteger
      每个Link节点默认能存储16个元素,通过继承AtomicInteger是为了解决分配线程通过WeakOrderQueue和回收线程归还对象时的线程竞争。

    整个Recycler回收对象存储结构如下图所示:

    5. Recycler实现细节

    本节部分内容摘录自[NettyRecycler(https://www.jianshu.com/p/4eab8450560c)。

    Recycler用来实现对象池,其中对应堆内存和直接内存的池化实现分别是PooledHeapByteBuf和PooledDirectByteBuf。Recycler主要提供了3个方法:

    • get():获取一个对象。
    • recycle(T, Handle):回收一个对象,T为对象泛型。
    • newObject(Handle):当没有可用对象时创建对象的实现方法。

    Recycler的UML图如下:

    下面分析下源码,首先看下Recycler.recycle(T, Handle)方法,用于回收1个对象:

    public final boolean recycle(T o, Handle handle) {
        if (handle == NOOP_HANDLE) {
            return false;
        }
    
        DefaultHandle h = (DefaultHandle) handle;
        if (h.stack.parent != this) {
            return false;
        }
        if (o != h.value) {
            throw new IllegalArgumentException("o does not belong to handle");
        }
        h.recycle();
        return true;
    }
    

    回收1个对象会调用该对象DefaultHandle.recycle()方法,如下:

     public void recycle() {
        stack.push(this);
     }
    

    回收1个对象(DefaultHandle)就是把该对象push到stack中。

    void push(DefaultHandle item) {
            Thread currentThread = Thread.currentThread();
            if (thread == currentThread) {
                // The current Thread is the thread that belongs to the Stack, we can try to push the object now.
                /**
                 * 如果该stack就是本线程的stack,那么直接把DefaultHandle放到该stack的数组里
                 */
                pushNow(item);
            } else {
                // The current Thread is not the one that belongs to the Stack, we need to signal that the push
                // happens later.
                /**
                 * 如果该stack不是本线程的stack,那么把该DefaultHandle放到该stack的WeakOrderQueue中
                 */
                pushLater(item, currentThread);
            }
        }
    

    这里分为两种情况,当stack是当前线程对应的stack时,执行pushNow(item)方法,直接把对象放到该stack的DefaultHandle数组中,如下:

        /**
         * 直接把DefaultHandle放到stack的数组里,如果数组满了那么扩展该数组为当前2倍大小
         * @param item
         */
        private void pushNow(DefaultHandle item) {
            if ((item.recycleId | item.lastRecycledId) != 0) {
                throw new IllegalStateException("recycled already");
            }
            item.recycleId = item.lastRecycledId = OWN_THREAD_ID;
    
            int size = this.size;
            if (size >= maxCapacity || dropHandle(item)) {
                // Hit the maximum capacity or should drop - drop the possibly youngest object.
                return;
            }
            if (size == elements.length) {
                elements = Arrays.copyOf(elements, min(size << 1, maxCapacity));
            }
    
            elements[size] = item;
            this.size = size + 1;
        }
    

    当stack是其它线程的stack时,执行pushLater(item, currentThread)方法,将对象放到WeakOrderQueue中,如下:

    private void pushLater(DefaultHandle item, Thread thread) {
           /** 
            * Recycler有1个stack->WeakOrderQueue映射,每个stack会映射到1个WeakOrderQueue,这个WeakOrderQueue是该stack关联的其它线程WeakOrderQueue链表的head WeakOrderQueue。
            * 当其它线程回收对象到该stack时会创建1个WeakOrderQueue中并加到stack的WeakOrderQueue链表中。 
            */
            Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
            WeakOrderQueue queue = delayedRecycled.get(this);
            if (queue == null) {
                /**
                 * 如果delayedRecycled满了那么将1个伪造的WeakOrderQueue(DUMMY)放到delayedRecycled中,并丢弃该对象(DefaultHandle)
                 */
                if (delayedRecycled.size() >= maxDelayedQueues) {
                    // Add a dummy queue so we know we should drop the object
                    delayedRecycled.put(this, WeakOrderQueue.DUMMY);
                    return;
                }
                // Check if we already reached the maximum number of delayed queues and if we can allocate at all.
                /**
                 * 创建1个WeakOrderQueue
                 */
                if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {
                    // drop object
                    return;
                }
                delayedRecycled.put(this, queue);
            } else if (queue == WeakOrderQueue.DUMMY) {
                // drop object
                return;
            }
    
            /**
             * 将对象放入到该stack对应的WeakOrderQueue中
             */
            queue.add(item);
        }
    
    static WeakOrderQueue allocate(Stack<?> stack, Thread thread) {
            // We allocated a Link so reserve the space
            /**
             * 如果该stack的可用共享空间还能再容下1个WeakOrderQueue,那么创建1个WeakOrderQueue,否则返回null
             */
            return reserveSpace(stack.availableSharedCapacity, LINK_CAPACITY)
                    ? new WeakOrderQueue(stack, thread) : null;
        }
    

    WeakOrderQueue的构造函数如下,WeakOrderQueue实现了多线程环境下回收对象的机制,当由其它线程回收对象到stack时会为该stack创建1个WeakOrderQueue,这些由其它线程创建的WeakOrderQueue会在该stack中按链表形式串联起来,每次创建1个WeakOrderQueue会把该WeakOrderQueue作为该stack的head WeakOrderQueue:

    private WeakOrderQueue(Stack<?> stack, Thread thread) {
            head = tail = new Link();
            owner = new WeakReference<Thread>(thread);
            /**
             * 每次创建WeakOrderQueue时会更新WeakOrderQueue所属的stack的head为当前WeakOrderQueue, 当前WeakOrderQueue的next为stack的之前head,
             * 这样把该stack的WeakOrderQueue通过链表串起来了,当下次stack中没有可用对象需要从WeakOrderQueue中转移对象时从WeakOrderQueue链表的head进行scavenge转移到stack的对DefaultHandle数组。
             */
            synchronized (stack) {
                next = stack.head;
                stack.head = this;
            }
            availableSharedCapacity = stack.availableSharedCapacity;
        }
    

    下面再看Recycler.get()方法:

    public final T get() {
        if (maxCapacity == 0) {
            return newObject(NOOP_HANDLE);
        }
        Stack<T> stack = threadLocal.get();
        DefaultHandle handle = stack.pop();
        if (handle == null) {
            handle = stack.newHandle();
            handle.value = newObject(handle);
        }
        return (T) handle.value;
    }
    

    取出该线程对应的stack,从stack中pop出1个DefaultHandle,返回该DefaultHandle的真正对象。
    下面看stack.pop()方法:

    DefaultHandle pop() {
            int size = this.size;
            if (size == 0) {
                if (!scavenge()) {
                    return null;
                }
                size = this.size;
            }
            size --;
            DefaultHandle ret = elements[size];
            elements[size] = null;
            if (ret.lastRecycledId != ret.recycleId) {
                throw new IllegalStateException("recycled multiple times");
            }
            ret.recycleId = 0;
            ret.lastRecycledId = 0;
            this.size = size;
            return ret;
        }
    

    如果该stack的DefaultHandle数组中还有对象可用,那么从该DefaultHandle数组中取出1个可用对象返回,如果该DefaultHandle数组没有可用的对象了,那么执行scavenge()方法,将head WeakOrderQueue中的head Link中的DefaultHandle数组转移到stack的DefaultHandle数组,scavenge方法如下:

    boolean scavenge() {
            // continue an existing scavenge, if any
            if (scavengeSome()) {
                return true;
            }
    
            // reset our scavenge cursor
            prev = null;
            cursor = head;
            return false;
        }
    

    具体执行了scavengeSome()方法,清理WeakOrderQueue中部分DefaultHandle到stack,每次尽可能清理head WeakOrderQueue的head Link的全部DefaultHandle,如下:

    boolean scavengeSome() {
            WeakOrderQueue cursor = this.cursor;
            if (cursor == null) {
                cursor = head;
                if (cursor == null) {
                    return false;
                }
            }
    
            boolean success = false;
            WeakOrderQueue prev = this.prev;
            do {
                /**
                 * 将当前WeakOrderQueue的head Link的DefaultHandle数组转移到stack的DefaultHandle数组中
                 */
                if (cursor.transfer(this)) {
                    success = true;
                    break;
                }
    
                WeakOrderQueue next = cursor.next;
                if (cursor.owner.get() == null) {
                    if (cursor.hasFinalData()) {
                        for (;;) {
                            if (cursor.transfer(this)) {
                                success = true;
                            } else {
                                break;
                            }
                        }
                    }
                    if (prev != null) {
                        prev.next = next;
                    }
                } else {
                    prev = cursor;
                }
    
                cursor = next;
    
            } while (cursor != null && !success);
    
            this.prev = prev;
            this.cursor = cursor;
            return success;
        }
    

    WeakOrderQueue.transfer()方法如下,将WeakOrderQueue的head Link中的DefaultHandle数组迁移到stack中:

    boolean transfer(Stack<?> dst) {
            Link head = this.head;
            if (head == null) {
                return false;
            }
    
            /**
             * 如果head Link的readIndex到达了Link的容量LINK_CAPACITY,说明该Link已经被scavengge完了。
             * 这时需要把下一个Link作为新的head Link。
             */
            if (head.readIndex == LINK_CAPACITY) {
                if (head.next == null) {
                    return false;
                }
                this.head = head = head.next;
            }
    
            final int srcStart = head.readIndex;
            /**
             * head Link的回收对象数组的最大位置
             */
            int srcEnd = head.get();
            /**
             * head Link可以scavenge的DefaultHandle的数量
             */
            final int srcSize = srcEnd - srcStart;
            if (srcSize == 0) {
                return false;
            }
    
            final int dstSize = dst.size;
    
            /**
             * 每次会尽可能scavenge整个head Link,如果head Link的DefaultHandle数组能全部迁移到stack中,stack的DefaultHandle数组预期容量
             */
            final int expectedCapacity = dstSize + srcSize;
            /**
             * 如果预期容量大于stack的DefaultHandle数组最大长度,说明本次无法将head Link的DefaultHandle数组全部迁移到stack中
             */
            if (expectedCapacity > dst.elements.length) {
                final int actualCapacity = dst.increaseCapacity(expectedCapacity);
                srcEnd = min(srcStart + actualCapacity - dstSize, srcEnd);
            }
    
            if (srcStart != srcEnd) {
                /**
                 * head Link的DefaultHandle数组
                 */
                final DefaultHandle[] srcElems = head.elements;
                /**
                 * stack的DefaultHandle数组
                 */
                final DefaultHandle[] dstElems = dst.elements;
                int newDstSize = dstSize;
                /**
                 * 迁移head Link的DefaultHandle数组到stack的DefaultHandle数组
                 */
                for (int i = srcStart; i < srcEnd; i++) {
                    DefaultHandle element = srcElems[i];
                    if (element.recycleId == 0) {
                        element.recycleId = element.lastRecycledId;
                    } else if (element.recycleId != element.lastRecycledId) {
                        throw new IllegalStateException("recycled already");
                    }
                    srcElems[i] = null;
    
                    if (dst.dropHandle(element)) {
                        // Drop the object.
                        continue;
                    }
                    element.stack = dst;
                    dstElems[newDstSize ++] = element;
                }
    
                /**
                 * 当head节点的对象全都转移给stack后,取head下一个节点作为head,下次转移的时候再从新的head转移回收的对象
                 */
                if (srcEnd == LINK_CAPACITY && head.next != null) {
                    // Add capacity back as the Link is GCed.
                    reclaimSpace(LINK_CAPACITY);
    
                    this.head = head.next;
                }
                /**
                 * 迁移完成后更新原始head Link的readIndex
                 */
                head.readIndex = srcEnd;
                if (dst.size == newDstSize) {
                    return false;
                }
                dst.size = newDstSize;
                return true;
            } else {
                // The destination stack is full already.
                return false;
            }
        }

     

    展开全文
  • CMapPtrToPtr的内存管理问题 CMapPtrToPtr类保存的是若干个映射项的集合。每个映射项保存了一对映射关系,一个称为键(key),相当于数学中的 x,另一个称为值(value),相当于y。为了将这些映射关系连在一起,...
  • 【JAVA 网络编程系列】Netty -- 内存管理(池化内存) 【1】jemalloc 简介 【1.1】内存池简介 内存池是指应用程序向操作系统(或 JVM)申请一块内存,对象的创建和销毁都从这块内存中分配和回收,对应地,管理这块...
  • netty池化内存分配

    2019-06-26 22:58:14
    1.计算内存块需要的大小-》计算内存块在缓存池的位置-》是否找到相应的位置-》找到位置则尝试从缓存池中取...2.event-loop pool池化中有三个区tiny(16b-512b),small(1kb,4kb),normal(8kb,32kb) 3.内存回收:relase() ...
  • 前言本文简要梳理为什么使用池化内存?Netty使用池化内存从哪些方面提升了效率?梳理了池化内存的核心组件大体含义以及内存分配流程,勾勒池化内存的整体框架。后面文章会详细拆解每个点是如何实现...
  • 前言非池化内存的分配由UnpooledByteBufAllocator负责,本文梳理下由其负责分配的堆内存和堆外内存如何实现的 。Netty在非池化内存分配上Java9与Java8以下版...
  • 【JAVA 网络编程系列】Netty -- 内存管理(非池化内存) 【1】Netty 内存相关类的继承结构 【1.1】ByteBuf 类的继承结构 内存实现方式,Heap(堆内) 和 Direct(堆内);是否池化,Pooled(池化) 和 Unpooled(非池化);...
  • 8.池化内存分配

    2020-01-19 20:56:28
    netty内存管理思想 PooledByteBufAllocate PoolChunk Chunk初始化 PoolChunk分配内存 netty内存管理思想 java作为一门拥有GC机制的语言,长久以来它的使用者都不必手动管理内存,这比起c/c 是一个巨大的进步...
  • 池化

    2018-09-12 17:30:24
    池化技术应用广泛,如内存池,线程池,连接池等等。内存池相关的内容,建议看看Apache、Nginx等开源web服务器的内存池实现。   起因:由于在实际应用当中,分配内存、创建进程、线程都会设计到一些系统调用,系统...
  • 在 Python 中经常通过内存池化技术来提高其性能,那么问题来了,在什么情况下会池化呢? 让我们通过几个例子进行一下理解一下. 预备知识 在查看例子之前,首先要提 python 中的一个函数 id(),让我们看一下函数说明: id...
  • 下面提出一种池化思想,通过进行内存管理,来减少GC。中心思想是把释放的东西保存起来管理,要用时直接取出使用。 下面直接来看代码: public abstract class PoolObject { public IObjectPool _pool; public int ...
  • 池化组件之内存

    2021-04-09 22:58:23
    1 内存池介绍 池化组件对于应用程序来说是缓冲器,我们应用程序开辟释放空间都会向操作系统请求,当多处进行开辟释放空间,程序员也不好维护,出现问题很难定位,尤其内存泄露。所以会引入内存池。 比如: 对于一款...
  • 字符串池化 python

    2018-12-28 15:12:00
    在 Python 中经常通过内存池化技术来提高其性能,那么问题来了,在什么情况下会池化呢? 让我们通过几个例子进行一下理解一下. 预备知识 在查看例子之前,首先要提 python 中的一个函数 id(),让我们看一下函数说明: id...
  • 吃透Netty源码系列二十九之池化内存分配总结申请缓冲区创建PoolThreadLocalCache创建PoolThreadCache获取线程本地变量的PoolArena进行分配尝试用缓存来分配缓存分配失败,走正常分配流程释放缓冲区创建实体放入队列...
  • 一些C ++魔术可帮助减少简单情况下的堆滥用
  • (二) netty非池化内存的表示:UnpooledHeapByteBuf和UnpooledDirectByteBuf ,介绍了各种非池化字节缓冲的分配、使用和回收。但在实践中一般不是直接调用他们的构造器来创建一个字节缓存的,而是通过分配器来分配,...
  • 吃透Netty源码系列二十一之池化内存分配二PoolChunk的newChunkPoolChunk的一些属性PoolChunk构造方法newSubpageArray创建化子页数组allocate分配空间allocateRun大于页大小的分配,即Normal类型log2高效的取出2的...
  • 与很多依赖于标记-清理的垃圾回收器的语言一样,C#也会在频繁分配内存或分配大块内存时产生性能问题。微软必应的高级软件开发工程师Ben Watson,就曾在使用MemoryStream类的时候遇到了这种问题。在他的Writing High-...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 19,608
精华内容 7,843
关键字:

内存池化