精华内容
下载资源
问答
  • 2020-12-01 10:51:40

    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>)都是虚引用的用法
    更多相关内容
  • 真实内存指的是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花的时间, 可以看出池化效果非常明显,而且十分平和
    capacityHeapBufferDirectBuffer池化的DirectBuffer
    64Byte465132112059
    256Byte946150742309
    512Byte2528195162188
    1024Byte4393219282044
    1. 启一个DiscardServer,然后发送80G的数据,看下GC次数,效果感人
    非池化池化池化+COMPOSITE_CUMULATOR
    208270

     

    展开全文
  • netty4的内存池化技术

    2021-03-08 23:25:21
    用户创建对象时,系统会在堆中分配空间存放对象,如果堆空间不足就触发JVM的gc做内存回收,在删除无效对象的同时对堆空间做整理,清除内存碎片使空闲内存连续,用于支持大对象的分配。脏活累活都让JVM干了,用户只要...

    此文已由作者徐赟授权网易云社区发布。

    欢迎访问网易云社区,了解更多网易技术产品运营经验。

    一、 前言

    java语言在运行于JVM的前提下,内存分配和回收通常无需用户干预。用户创建对象时,系统会在堆中分配空间存放对象,如果堆空间不足就触发JVM的gc做内存回收,在删除无效对象的同时对堆空间做整理,清除内存碎片使空闲内存连续,用于支持大对象的分配。脏活累活都让JVM干了,用户只要专注自己的逻辑,java应用就能长时间的以最优状态持续运行。

    这是java语言和JVM的核心优势,但在一些场景下,完全剥离用户对内存和对象生命周期的管理也会造成一些困扰:

    1. 对象生命周期的开始由用户控制,但用户却完全不知道生命周期何时结束。类似于C++中的析构函数,java中提供finalize方法,但该方法并不会在对象失效后而是在gc触发时被调用。gc的触发时间对用户来说是玄之又玄的东西,即使显式调用System.gc()方法也不一定会触发gc,因为该方法只是建议JVM做gc,至于是否真的要做,还要依赖JVM自身的判断。因此想要在finalize方法里释放资源的同学要失望了,因为资源可能会在任意一个用户无法控制的时间被释放(ps. 使用reference能够探测对象回收的时机,但会对回收造成压力,极端情况下会导致内存溢出)。且不说finalize方法执行机制存在安全隐患,这又是另一个问题,总之不建议依赖finalize方法实现对象回收行为。目前对象生命周期管理的一种思路,是将对象交于容器管理,通过容器控制对象生命周期。

    2. java中没有指针,也不方便用指针,这表示用户对内存的控制能力非常弱,换来的是代码的整洁和健壮。在绝大多数java面临的业务场景下,用户无需关心内存的具体使用方式。但对于有海量数据流转的java后台应用,锯齿状的内存使用曲线和周期性的大量gc,频繁的对象创建和回收,依然会或多或少的带来一些性能开销,这里有优化的余地,就是使用内存池化技术。

    二、 池化技术概述

    池化的简单实现思路,是基于JVM堆内存之上,构建更高一层内存池,通过调用内存池allocate方法获取内存空间,调用release方法将内存区域归还内存池。内存池面临的首要问题是碎片回收,内存池在频繁申请和释放空间后,还能有尽可能连续的内存空间用于大块内存空间的分配。基于这个需求,有两种算法用于优化这一块的内存分配:伙伴系统和slab系统。

    伙伴系统:

    伙伴系统是以类完全二叉树的结构组织内存区域,左右节点互为伙伴。内存分配过程中,大块内存不断二分,直到找到满足所需的最小内存分片。内存释放是,判断释放内存分片的伙伴是否空闲,如果空闲则将左右伙伴合成更大一级内存块。linux就是使用该方式解决外部分配碎片的问题,为避免分片太细碎,通常情况下有最小分片,例如4k。

    slab系统:

    slab系统主要解决内部碎片问题,slab系统的思路是将预先申请的一块内存区域包装成一个内存集,该内存集将申请到的大块内存分割成相等大小的内存片。用户申请小块内存时,通过具体申请的内存大小找到slab系统中对应的一个内存集,从内存集中拿到内存分片,内存释放时也是将内存分片归还给内存集。

    三、 netty4池化技术实现

    netty4相对于netty3的一大改进就是引入了内存池化技术,用以解决高速网络通信过程中,netty造成的应用内存锯齿状消费和大量gc的问题。这一块代码内容很多,逻辑略显复杂,但究其根本就是伙伴系统和slab系统的实现和扩展。内存池管理单元以Allocate对象的形式出现,一个Allocate对象由多个Arena组成,每个Arena能完全执行内存块的分配和回收。Arena内有三类内存块管理单元:TinySubPage,SmallSubPage,ChunkList。其中tiny和small符合slab系统的管理策略,ChunkList符合伙伴系统的管理策略。当用户申请内存介于tinySize和smallSize之间时,从tinySubPage中获取内存块;申请内存介于smallSize和pageSize之间时,从smallSubPage中获取内存块;介于pageSize和chunkSize之间时,从ChunkList中获取内存;大于ChunkSize的内存块不通过池化分配(其中tinySubPage和smallSubPage的内存也来自ChnkList,是对一个page内存块的细分)。

    除此之外,netty4中还有线程缓存的内存块和实现Recycler的对象重用,这些内容与池化无关,暂不赘述。

    java代码中可以使用netty4中的PooledByteBufAllocator对象实现内存池化效果。

    四、 可能存在的内存泄露问题

    用户自己管理内存带来的弊端就是可能存在内存泄露的问题,持续的内存泄露会造成应用性能下降,严重的还会导致oom,另外内存泄露问题往往难以发现。

    合理使用netty4中的内存池,需要账务netty4中的以下特性:

    1. 线程缓存问题

    netty4中的池化buf,内存使用结束后首先将内存块归还给本地缓存,便于本地再次申请时直接从本地缓存获取,降低多线程对集中式内存分配器的并发压力,这时如果缓存分配跨线程就会出现问题。例如线程A申请一块内存空间存放数据,这一块内存随后流转到线程B,线程B释放内存块,此时内存块被缓存在B线程中,A再申请一块内存空间时,线程缓存中任然没有,而由于线程B从未申请内存,因此B中缓存的内存块永远不会被用到(线程缓存中空闲内存块清理工作有allocate方法触发,因此B线程如果从不申请内存块则线程缓存永远不会被清理)

    创建Allocator时有一个useCacheForAllThreads变量,用于控制缓存块是否被线程缓存,默认时开启的。如果内存块要在多个线程中流转,可以考虑关闭该变量,防止内存泄露。

    2. 死亡线程的内存块回收

    netty为了应对内存泄露的问题,在创建PoolThreadCache对象时,通过

    ThreadDeathWatcher.watch(deathWatchThread, freeTask);

    启用守护线程检测线程存活状态,随后每个PoolThreadCache对象创建时都会把当前线程注册进来。守护线程会每秒检测一次,判断线程死亡了则回收线程缓存中的内存块。

    3. 对象缓存

    netty4中的缓存池不仅缓存内存块,还缓存ByteBuf对象。

    例如allocate的一个PooledUnsafeHeapByteBuf对象,在release后会被暂存在RECYCLER中,再次allocate会将该对象重新分配使用,这样会造成之前已经release的对象重新变得可用。

    ByteBuf byteBuf = bufAllocator.buffer(100);

    byteBuf.release();

    ByteBuf byteBuf1 = bufAllocator.buffer(100);

    此时byteBuf会重新变得可用。

    4. 内存泄露检测

    当前有4个泄露检测级别:

    ● 禁用(DISABLED)   - 完全禁止泄露检测。不推荐。

    ● 简单(SIMPLE)       - 告诉我们取样的1%的缓冲是否发生了泄露。默认。

    ● 高级(ADVANCED) - 告诉我们取样的1%的缓冲发生泄露的地方

    ● 偏执(PARANOID)  - 跟高级选项类似,但此选项检测所有缓冲,而不仅仅是取样的那1%。此选项在自动测试阶段很有用。如果构建(build)输出包含了LEAK,可认为构建失败。

    使用 -Dio.netty.leakDetectionLevel=advanced 配置

    网易云免费体验馆,0成本体验20+款云产品!

    更多网易技术、产品、运营经验分享请点击。

    展开全文
  • 2)直接内存 vs 堆内存 可以使用下面的代码来创建池化基于堆的 ByteBuf ...直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用 直接内存对 GC 压力小,因为这部分内存

    2)直接内存 vs 堆内存

    可以使用下面的代码来创建池化基于堆的 ByteBuf

    ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
    

    也可以使用下面的代码来创建池化基于直接内存的 ByteBuf

    ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
    
    • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
    • 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放

    3)池化 vs 非池化

    池化的最大意义在于可以重用 ByteBuf,优点有

    • 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
    • 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
    • 高并发时,池化功能更节约内存,减少内存溢出的可能

    池化功能是否开启,可以通过下面的系统环境变量来设置

    -Dio.netty.allocator.type={unpooled|pooled}
    
    • 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
    • 4.1 之前,池化功能还不成熟,默认是非池化实现
    展开全文
  • 无论是池化的ByteBuf还是非池化的ByteBuf,BuyeBuf(不一定是该对象)对象在被gc回收之后,通过判断refCount是否为0来判断是否发生了内存泄漏。 netty支持下面四种级别,使用-Dio.netty.leakDetectionLevel=...
  • 文章目录Netty内存分配Netty内存分配对象Netty内存池化过程Netty内存划分——Arena参考资料 Netty内存分配 如果是在堆上分配内存空间将会触发频繁的 GC,在堆外内存使用方面,Netty 自己实现了一套创建、回收堆外...
  • 池化组件之内存

    2021-07-29 23:33:17
    1 解决问题 对于服务器而言,当epoll_wait检查io事件是否就绪后,然后recv接收数据写入buffer,对于buffer如果采用在栈上处理;会出现如果耗时,一次性...每个进程都有一个对应的虚拟内存,和真实的物理内存一样大都是4G
  • 在netty的池化ByteBuf分配中,包含ByteBuf对象的池化和真实内存(array或者DirectByteBuffer)的池化。 实际上Recycler不仅可以用于ByteBuf对象的池化,他是一个通用的对象池化技术,我们可以直接使用Recycler实现...
  • 【JAVA 网络编程系列】Netty -- 内存管理(池化内存) 【1】jemalloc 简介 【1.1】内存池简介 内存池是指应用程序向操作系统(或 JVM)申请一块内存,对象的创建和销毁都从这块内存中分配和回收,对应地,管理这块...
  • 在PooledByteBufAllocator类中有对应的PoolArena数组,此数组时与线程数相关的,目的是为了减少在多线程分配内存时线程竞争。 public static final PooledByteBufAllocator DEFAULT = new PooledByteBufAllocator...
  • 2.静态内存分配和动态内存分配二、malloc实现原理malloc内存分配(下面算是正常一般的情况了)三、使用C语言实现一个内存池1.C语言实现一个内存池 一、基础概念 在讲内存池技术之前,咱们先对一些基础概念进行阐述,...
  • 一、池化技术-简单点来说,就是提前保存大量的资源,以备不时之需。 对于线程,内存,oracle的连接对象等等,这些都是资源,程序中当你创建一个线程或者在堆上申请一块内存时,都...池化技术主要有线程池,内存池...
  • 池化技术】池化技术基础和原理

    千次阅读 2021-12-12 23:01:43
    内存池三、实例 一、概念 池化技术指的是提前准备一些资源,在需要时可以重复使用这些预先准备的资源。 在系统开发过程中,我们经常会用到池化技术。通俗的讲,池化技术就是:把一些资源预先分配好,组织到对象池中...
  • 内存池是池化技术的一种应用。所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变...
  • Netty 非池化内存分配

    2021-03-09 20:39:47
    前言非池化内存的分配由UnpooledByteBufAllocator负责,本文梳理下由其负责分配的堆内存和堆外内存如何实现的 。Netty在非池化内存分配上Java9与Java8以下版本有啥不同呢?Netty堆外内存回收默认机制使用JDK提供的...
  • 【概念】内存池简介

    千次阅读 2019-12-01 19:47:45
    池化技术 池 是在计算机技术中经常使用的一种设计模型,其内涵在于:将**程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量。**...
  • 前言非池化内存的分配由UnpooledByteBufAllocator负责,本文梳理下由其负责分配的堆内存和堆外内存如何实现的 。Netty在非池化内存分配上Java9与Java8以下版...
  • MFC基于CPlex结构的内存池化管理

    千次阅读 2010-06-17 22:16:00
    内存池化管理( Memory Pool )。 写一个分配内存的全局函数,每一次调用此函数都可以获得一个指定大小的 内存块 来容纳 多个 CAssoc 结构。另外,还 必须要有一种机制将此函数申请的内存块记录下来 ,以便当...
  • 一些C ++魔术可帮助减少简单情况下的堆滥用
  • 这两篇文章我觉得可以很好的帮助理解池化内存一些相关内容。 这个池化内存我们是如何使用的呢? 1.首先来到NioSocketChannle 读取数据部分: if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT))...
  • 【JAVA 网络编程系列】Netty -- 内存管理(非池化内存) 【1】Netty 内存相关类的继承结构 【1.1】ByteBuf 类的继承结构 内存实现方式,Heap(堆内) 和 Direct(堆内);是否池化,Pooled(池化) 和 Unpooled(非池化);...
  • 在 Python 中经常通过内存池化技术来提高其性能,那么问题来了,在什么情况下会池化呢? 让我们通过几个例子进行一下理解一下. 预备知识 在查看例子之前,首先要提 python 中的一个函数 id(),让我们看一下函数说明: id...
  • 一.安装 tcmalloc (google-perftools) 是用于优化C++写的多线程应用 ...但是要注意一点:对于类对象进行申请内存和释放内存时,不会调用构造函数和析构函数.可以在类中写一个reset()函数来实现调用构造函数和析构函数.
  • 堆外内存和堆内内存 池化和非池化 我们在利用Netty做底层通信框架的时候,会默认给我们的到底是哪一种组合了? 分析 池化分析 Netty的Boostrap启动类按照标准模板,通常会添加这个配置option(ChannelOption....
  • 我们上几篇文章分析了池化内存的使用:PoolChunk和PoolSubPage 我们通过代码看到了池化内存后将一些信息都记录到了ByteBuf的实现类上。在allocat()方法之前都会调用newByteBuf()返回一个实例,那个在这个过程干了...
  • Netty的ByteBuf是如何支持堆内存池化实现的 ByteBuffer 从实现方式上分成 HeapByteBuffer 和 DirectByteBuffer 两种内存实现方式, HeapByteBuffer 底层使用 byte 数组存储数据, DirectByteBuffer 底层使用 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 23,866
精华内容 9,546
关键字:

内存池化

友情链接: annotate.rar