从上篇文章(六) 图解netty内存池的核心分配算法—PoolArena/PoolChunk/PoolSubpage中可以知道,netty内存池在分配粒度为tiny/small/normal的内存块时,都首先尝试从线程缓冲池PoolThreadCache分配,线程缓冲分配不成功,才分配新的内存。这么做可以明显地提高内存分配的性能。
那么现在来分析,线程缓冲池中存的是什么?怎么存进去?又怎么取出来?
PoolThreadCache的主要属性如下:
final PoolArena<byte[]> heapArena;
final PoolArena<ByteBuffer> directArena;
// Hold the caches for the different size classes, which are tiny, small and normal.
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<byte[]>[] normalHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;
private int allocations;
属性的概要说明
heapArena和poolArena这两个属性跟后面说的从缓冲池分配还是两码事,因为它们是表明当前请求分配内存的线程应该从哪个Arena上分配,如之前文章所说,每个通过给每个线程分配Arena,来减小资源竞争。
六个MemoryRegionCache[]数组,缓存的是之前已经分配出去的但是已经不再使用的内存,好处是如果后面需要分配相同粒度的内存,直接从缓冲池分配,以提高效率。tiny和small数组,与Arena中的tinySubpagePools数组和smallSubpagePools数组相对应:tiny数组的大小是32,使用的index从1开始,依次代表大小为16 bytes、32 bytes、48 bytes、… 、496 bytes的内存块;small数组大小为4,一次代表大小为512 bytes、1024 bytes、2048 bytes、4096 bytes的内存块。
而small数组大小是3,但只用到index=1和2两个位置。index=1缓存的内存块大小是1个page大小,即8k。index=2的位置缓存的内存块大小是2个page大小,即16k。
MemoryRegionCache内部有个队列,存放被缓存的内存块,如下:
private final int size;
private final Queue<Entry<T>> queue;
private final SizeClass sizeClass;
private int allocations;
size是队列queue的大小,对于tiny类型,队列大小是512,small的是256,normal的是64;queue是缓存内存块的队列,queue内的每个Entry对象表示被缓存的内存;sizeClass有tiny/small/normal三个取值,表明当前缓冲所缓存的内存的粒度分类;allocations表示从该缓冲分配的次数。
结合以上属性分析,可知PoolThreadCache对象的每个数组的拓扑结构如下:
创建的时机
当线程初次调用PooledByteBufAllocator的allocate方法,申请分配内存时,会创建PoolThreadCache对象,并放到线程本地存储变量PoolThreadLocalCache中,后面每次使用时,从PoolThreadLocalCache中拿出再使用。(PoolThreadLocalCache继承自FastThreadLocal,可参考:图解netty:FastThreadLocal实现原理分析。
创建PoolThreadCache时,会分别为其分配一个heapArena和directArena,并初始化其各个缓冲数组。
内存块什么时候会被放到PoolThreadCache?
我们知道,当ByteBuf的引用计数器为0时,会调用deallocation()方法,释放掉内存。这时如果ByteBuf是个PooledByteBuf对象,就会调用其所属的Arena的free方法来释放内存。
在Arena的free方法中,首先尝试调用PoolThreadCache的add方法,将内存块缓存到其中:
SizeClass sizeClass = sizeClass(normCapacity);
if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
// cached so not free it.
return;
}
如果缓冲未满,那就会成功将内存块放到缓冲里面。
对于何时从缓冲分配内存,上一篇文章已经说明,在分配内存时都先尝试从Cache分配,能分配成功那就结束分配过程。
既然缓冲有容量限制,那么就必定有相应的清理策略,见下一小节。
何时清理缓冲里的内存
在PoolThreadCache的数据结构一节,可以看到它有一个allocations属性,同时还有个freeSweepAllocationThreshold属性:
private int allocations;
private final int freeSweepAllocationThreshold;
每从该Cache分配一次内存,allocations都增加一次,当次数达到freeSweepAllocationThreshold(默认是8192次)时,将allocations清零,并调用trim()方法清理缓存。这个清理动作,会清理每个数组每个MemoryRegionCache元素内的缓存。而对每个MemoryRegionCache的清理策略是:
(1)计算需要清理的内存块个数。在每个MemoryRegionCache内部,也有个分配计数器,使用队列的大小size减去该计数器,得到需要从MemoryRegionCache缓冲队列中清楚的内存块个数toFreeCount。
(2)从队列头部开始清理,一直清理toFreeCount个。
好了,对PoolThreadCache的讲解就到此结束了。由于时间关系,应该不会再写关于netty的文章了,其实netty内存池技术,还有两个重要的组件还没介绍:一个是Recycler,一个是内存泄露探测器ResourceLeakDetector。下面分别介绍它们的大概原理。
Recyler其实就是一个空对象的缓存,它的作用就是在应用需要某个类的一个对象时,优先从Recyler中分配,尽量避免调用new来创建新对象(只有从Recyler无法获取到空对象时,才调用new分配新对象)。当不再使用该类对象时,回收到Recyler中。
Recyler一般作为类的静态变量,专门缓存该类的空对象,所以从数据结构的关系上看,Recyler和它缓存对象的类是一对一的关系。
从Recyler的作用,可以推断到Recyler使用的时机也很明显。当对象被释放时,调用Recyler的recycle进行回收,Recyler会把这个对象缓存起来,其实就是放到它内部的一个stack栈里面,避免对象被GC回收。当需要该类的对象时,先从Recyler内部的stack里面获取,stack没有了再调用new关键字创建新对象。
在netty中,有两个重要场景使用到了Recycler,都与内存分配有关。一个是内存池的字节缓冲对象,PooledHeapByteBuf和PooledDirectByteBuf两个类分别有一个RECYCLER静态变量,以PooledDirectByteBuf源码为例:
final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {
private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() {
@Override
protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
return new PooledDirectByteBuf(handle, 0);
}
};
......
}
当需要PooledDirectByteBuf对象时,调用RECYCLER.get()方法获取,这个get()方法,会优先尝试从RECYCLER内部的stack获取PooledDirectByteBuf对象,获取不到,再调用RECYCLER的newObject方法,newObject方法会调用PooledDirectByteBuf构造器来创建出PooledDirectByteBuf对象。
当PooledDirectByteBuf对象的引用计数器减为0,调用deallocate()释放内存时,该PooledDirectByteBuf空对象会被放入到RECYCLER的stack中。
RECYCLER内部存放对象的stack,也是使用了ThreadLocal技术,每个线程会有一个。
**何为空对象?**这与具体的类有关,比如PooledDirectByteBuf,其对象如果持有内存,memory属性是会指向内存块的,如果将其memory属性置为null,就表明它不持有任何内存,那么它就是一个空对象。
netty中另外一个使用Recycler的场景,正是本文讲解的PoolThreadCache,具体略,读者感兴趣可自行研究其源码。
netty中,如果是通过内存池分配器PooledByteBufAllocator来分配ByteBuf,那么该ByteBuf肯定会有内存泄露探测功能。如果是通过非池化的分配器UnpooledByteBufAllocator来分配内存,那么这个功能是可选的。
netty内存泄露探测主要是用于内存池的场景,原理是:
(1)在成功分配一个ByteBuf后,创建一个弱引用指向该ByteBuf,创建弱引用时会同时使用一个引用队列。
(2)ByteBuf使用了引用计数器技术,当ByteBuf使用完之后,正常情况下引用计数器变为0,这时ByteBuf对应的内存会正常被归还到内存池中,这时不会发生内存泄露。
(3)但是如果ByteBuf使用完之后没有调用其release()方法,那么其引用计数器就不会减少到0,其内存也就不会归还到内存池中。而JVM的垃圾收集器是不会感知该计数器的,计数器只是netty自己内部的东西,所以只要没有对象引用该ByteBuf了,这个对象就会被回收掉。这里,内存泄露就发生了:ByteBuf占用的内存还没归还到内存池中,就被回收掉了,那么它所占用的内存段,一直标识为已占用,但是其实没任何东西占用它。
(4)泄露探测:由于在创建ByteBuf时,同时创建了一个指向它的弱引用,以及使用了一个引用队列。当ByteBuf被GC回收时,这个弱引用就会被放到引用队列中,所以,netty只需要观察这个引用队列是否有元素,就可以知道是否发生了内存泄露。
全文完~