netty5学习笔记-内存池5-PoolThreadCache

来源:互联网 发布:linux 配置环境变量 编辑:程序博客网 时间:2024/04/29 15:30

        看了前面的PoolArena分析,我们知道PoolArena在分配内存时,使用了synchronized来保证线程安全,这样就带来了一定的效率问题。如何能再此基础上再优化呢,答案很简单,使用ThreadLocal类似的解决方案避免加锁! 为什么可以使用ThreadLocal,如果了解过netty线程模型(有兴趣后续可以一起学习),应该知道netty的woker线程池,它负责了数据的接收、发送(编解码这里先不说),并且保证同一个连接在同一个线程中完成这两个步骤,因此很容易利用ThreadLocal进行内存的分配和回收优化。

        下面我们来看看内存池内存分配流程:

1、ByteBufAllocator 准备申请一块内存;

2、尝试从PoolThreadCache中获取可用内存,如果成功则完成此次分配,否则继续往下走,注意后面的内存分配都会加锁;

3、如果是小块(可配置该值)内存分配,则尝试从PoolArena中缓存的PoolSubpage中获取内存,如果成功则完成此次分配;

4、如果是普通大小的内存分配,则从PoolChunkList中查找可用PoolChunk并进行内存分配,如果没有可用的PoolChunk则创建一个并加入到PoolChunkList中,完成此次内存分配;

5、如果是大块(大于一个chunk的大小)内存分配,则直接分配内存而不用内存池的方式;

6、内存使用完成后进行释放,释放的时候首先判断是否和分配的时候是同一个线程,如果是则尝试将其放入PoolThreadCache,这块内存将会在下一次同一个线程申请内存时使用,即前面的步骤2;

7、如果不是同一个线程,则回收至chunk中,此时chunk中的内存使用率会发生变化,可能导致该chunk在不同的PoolChunkList中移动,或者整个chunk回收(chunk在q000上,且其分配的所有内存被释放);同时如果释放的是小块内存(与步骤3中描述的内存相同),会尝试将小块内存前置到PoolArena中,这里操作成功了,步骤3的操作中才可能成功。

       

        从前面的几个步骤,加上netty本身的线程模型,可以知道ThreadLocal被使用到的几率是很大的(本身的网络io都能用到)。

       ThreadLocal管理的对象为PoolThreadCache,由于tiny\small\normal在内存分配上的不同(tiny和small使用subpage,normal使用page),PoolThreadCache中也分了tinySubPageHeapCaches、smallSubPageHeapCaches、normalSubPageHeapCaches三个数组,数据的使用方式与PoolArena中的subpagePools相同,不同的index放不同大小的相应对象,这里数组中对应的类为MemoryRegionCache,来看看它的几个重要属性:

        private final Entry<T>[] entries;        private final int maxUnusedCached;        private int head;        private int tail;        private int maxEntriesInUse;        private int entriesInUse;        private static final class Entry<T> {            PoolChunk<T> chunk;            long handle;        }

entries:存放可分配内存的数组,entry维护一个chunk的一段内存标识;

head:当前第一个可分配内存在entries中的位置;
tail:当前数组中可以放数据的第一个可用位置;

        在初始化的时候head=tail=0, 随着cache中慢慢有数据加入,tail会不停的增加,当大于等于entries.length时回到0,形成一个环形数组。 同时每次往entries中放数据时,如果发现tail对应位置存在可用的chunk,则说明池已满,此时放弃加入cache。 随着不停分配内存的需求,head也会不停的增加,当其所在位置的entry为空时,说明cache中无可用内存,此时会进入上面的步骤3进行内存分配。

maxUnusedCached:entries.length/2

maxEntriesInUse: cache的最大同时使用数;

entriesInUse: cache中正在使用的内存数目;

如果cache的复用率不高,则会尝试对cahe中的数据进行收缩,每执行freeSweepAllocationThreshold次(可配置)内存分配进行一次收缩,逻辑如下:

1. free=cache中的可用内存段的数目 - 最大同时使用数,及在内存分配高峰期可能存在free个剩余的内存段;

2.如果free>maxUnusedCached,则说明cache中可用数量较多,而使用频率又比较低(maxEntriesInUse小),存在浪费的可能。此时从head开始释放掉free个内存段。

        用过ThreadLocal的同学都知道,使用不当很容易导致内存泄露,而在netty中则使用了ThreadDeathWatcher来防止内存泄露。ThreadDeathWatcher的逻辑很简单,每1s判断指定的Thread是否alive,如果线程已经停止,则执行指定的逻辑:

ThreadDeathWatcher.watch(thread, freeTask);// 如果thread停止则执行freeTaskprivate final Runnable freeTask = new Runnable() {        @Override        public void run() {            // 释放所有持有的内存            free0();        }    };

       到此,netty内存池直接相关的代码已经分析完,其实你会发现netty就是实现了两个比较经典的分配策略,buddy allocation(见PoolChunk)和jemalloc(有一定改动,PooledByteBufAllocator+PoolArena+PoolChunk+PoolThreadCache),所以如果想了解更新的信息,可以按照上面两个关键词搜索。





0 0
原创粉丝点击