在netty引用计数机制介绍中,我们说到了对象池,这里对对象池做一个详细的介绍。
这里引用上节的内容,介绍下对象池的作用。
对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象,类似线程池的概念。对象池缓存了一些已经创建好的对象,避免需要时才创建对象,同时限制了实例的个数。池化技术最终要的就是重复的使用池内已经创建的对象。从上面的内容就可以看出对象池适用于以下几个场景:
1.创建对象的开销大
2.会创建大量的实例
3.限制一些资源的使用
DirectByteBuffer的缺点在于分配和回收的的代价相对较大,因此DirectByteBuffer适用于缓冲区可以重复使用的场景。
在引用计数变为0的时候,会进行对象的回收,对于池化对象,我们需要将该对象放回到池中,否则会有内存泄露的问题。
内存泄漏:在netty中的内存泄漏是指GC自动回收了refCnt>0的对象的内存,这种情况下使得该对象无法被重建,同时也无法被归还回到netty对象池中,所以即使该内存空置可用,但是应用层仍然标志了该内存被使用中,从而使得该块内存在应用的整个生命周期中无法被使用,造成了浪费。
这里我们来看下netty对象池的实现是怎样的
这里拿PooledByteBuf这个类来作为入口,该类是池化bytebuf所要继承的抽象类,上次我们说到了它的deallocate()方法,用于在引用计数为0后释放资源
如图,可以看到其中两个变量,leak和recyclerHandle,其中leak用于内存泄露的监测,recyclerHandle负责对象在使用完毕后放回对象池中,后者使我们要详细去介绍的。我们看下recyclerHandle的外部类实现
可以看到doc介绍说是一个基于Threadlocal实现的轻量级对象池
我们来看一下它内部的成员变量有哪些,并做一些简单的介绍先:
/*
*唯一ID生成器
* 用在两处:
* 1、当前线程ID
* 2、WeakOrderQueue的id
*/
private static final AtomicInteger ID_GENERATOR = new AtomicInteger(Integer.MIN_VALUE);
private static final int OWN_THREAD_ID = ID_GENERATOR.getAndIncrement();
//Bytebuf最大容量及初始化容量,静态模块中是这两个变量的计算
private static final int DEFAULT_MAX_CAPACITY;
private static final int INITIAL_CAPACITY;
static {
// In the future, we might have different maxCapacity for different object types.
// e.g. io.netty.recycler.maxCapacity.writeTask
// io.netty.recycler.maxCapacity.outboundBuffer
int maxCapacity = SystemPropertyUtil.getInt("io.netty.recycler.maxCapacity.default", 0);
if (maxCapacity <= 0) {
// TODO: Some arbitrary large number - should adjust as we get more production experience.
maxCapacity = 262144;
}
DEFAULT_MAX_CAPACITY = maxCapacity;
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.recycler.maxCapacity.default: {}", DEFAULT_MAX_CAPACITY);
}
INITIAL_CAPACITY = Math.min(DEFAULT_MAX_CAPACITY, 256);
}
//存储Stack的最大容量
private final int maxCapacity;
//借助threadLocalmap保存栈
private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {
@Override
protected Stack<T> initialValue() {
return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacity);
}
};
/**
* 创建一个对象
* 1、由子类进行复写,所以使用protected修饰
* 2、传入Handle对象,对创建出来的对象进行回收操作
*/
protected abstract T newObject(Handle handle);
//处理回收的接口,实现也在该类
public interface Handle { }
//借助threadLocalmap保存由WeakOrderQueue组成的map
private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =
new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {
@Override
protected Map<Stack<?>, WeakOrderQueue> initialValue() {
return new WeakHashMap<Stack<?>, WeakOrderQueue>();
}
};
继续来看下其初始化方法
protected Recycler() {
this(DEFAULT_MAX_CAPACITY);
}
protected Recycler(int maxCapacity) {
this.maxCapacity = Math.max(0, maxCapacity);
}
//调用方式例如:
private static final Recycler<PooledHeapByteBuf> RECYCLER = new Recycler<PooledHeapByteBuf>() {
@Override
protected PooledHeapByteBuf newObject(Handle handle) {
return new PooledHeapByteBuf(handle, 0);
}
};
对象的获取
下面我们来看下对象是怎么从对象池中得到的
public final T get() {
Stack<T> stack = threadLocal.get();
DefaultHandle handle = stack.pop();
if (handle == null) {
//为空就创建DefaultHandle
handle = stack.newHandle();
//并创建对象赋值给DefaultHandle
handle.value = newObject(handle);
}
return (T) handle.value;
}
重点看下stack.pop()方法
//Stack
@SuppressWarnings({ "unchecked", "rawtypes" })
DefaultHandle<T> pop() {
int size = this.size;
//如果Stack当前数组大小为0,表示没有现成可用的对象,
//尝试使用scavenge进行回收,大致过程就是从链接的一个个WeakOrderQueue队列中取出对象使用transfer方法将对象放入到Stack容器数组中
if (size == 0) {
//简要概括scavenge流程就是Stack从“背后”的Queue中获取可用的实例,
//如果Queue中没有可用实例就遍历到下一个Queue(Queue组成了一个链表)
if (!scavenge()) {
//没有可回收对象,则返回null
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;
}
对象的回收
可以看到是调用了它的get方法用于对象的获取,那么对象的回收是怎么做的呢?
这里了我们可以看到,这里有个对象类型为DefaultHandle,这个就是其内部接口Handle的实现,这里我们来看下这个对象的实现
static final class DefaultHandle implements Handle {
/**
* 当前的DefaultHandle对象所属的Stack
*/
private Stack<?> stack;
/**
* 真正的对象,value与Handle一一对应
*/
private Object value;
DefaultHandle(Stack<?> stack) {
this.stack = stack;
}
public void recycle() {
Thread thread = Thread.currentThread();
//如果归还线程是栈归属的线程,就直接push入栈,完成回收
if (thread == stack.thread) {
stack.push(this);
return;
}
// we don't want to have a ref to the queue as the value in our weak map
// so we null it out; to ensure there are no races with restoring it later
// we impose a memory ordering here (no-op on x86)
Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
WeakOrderQueue queue = delayedRecycled.get(stack);
if (queue == null) {
delayedRecycled.put(stack, queue = new WeakOrderQueue(stack, thread));
}
queue.add(this);
}
}
DefaultHandle的recycle方法就是负责对象回收的
流程如下:
如果当前线程是当前stack对象的线程,那么将实例放入stack中(存储了对应类型的对象),否则:
获取当前线程对应的Map<Stack, WeakOrderQueue>,并将实例加入到Stack对应的Queue中。
下面看一下上述用的几个组件
Stack的构成
final Recycler<T> parent;
//所属线程
final Thread thread;
//存储的回收handle及对象的数组
private DefaultHandle[] elements;
private final int maxCapacity;
//栈位置
private int size;
private volatile WeakOrderQueue head;
private WeakOrderQueue cursor, prev;
其中看到了WeakOrderQueue对象,来看下对应的构成。
// chain of data items
private Link head, tail;
// pointer to another queue of delayed items for the same stack
private WeakOrderQueue next;
private final WeakReference<Thread> owner;
private final int id = ID_GENERATOR.getAndIncrement();
Link对象
private static final class Link extends AtomicInteger {
//固定大小为16的DefaultHandle数组
private final DefaultHandle[] elements = new DefaultHandle[LINK_CAPACITY];
private int readIndex;
private Link next;
}
WeakOrderQueue对象中存的就是一个Link构成的链表,当对象放满一个Link后,会在直接新建一个新的Link加入链表,这样做的目的是避免数组扩容带来的性能损耗,直接用链表将数组连接起来
WeakOrderQueue的作用是什么呢?
首先我们的池化对象被回收时,如果是被stack所属线程回收会直接放入stack中,如果是其他线程回收呢?
这里为了避免锁竞争,通过ThreadLocal给其他线程建立一个map,key就是stack本身而value则是WeakOrderQueue,
WeakOrderQueue里面存放的是link数组的链表,每一个数组都是一个DefaultHandle的数组(存放对象)。
另外属于某个stack的WeakOrderQueue会被串联成一个链表,链表的实现就是在创建WeakOrderQueue的时候进行的,如下
WeakOrderQueue(Stack<?> stack, Thread thread) {
head = tail = new Link();
owner = new WeakReference<Thread>(thread);
synchronized (stack) {
//将当前WeakOrderQueue的next指向之前的WeakOrderQueue
//然后再将stack指向当前WeakOrderQueue,从而形成了一个链表
next = stack.head;
stack.head = this;
}
}
整体逻辑结构
1.当我们调用其get方法的时候逻辑是从ThreadLocal中获取一个stack,该stack内部存储一个DefaultHandle数组,每个DefaultHandle内部都持有一个stack的引用,而每个线程都为一个ThreadLocal存储一个固定的stack。
每个EventLoop线程有一个stack(底层是数组实现),stack里面存储的是DefaultHandle数组,DefaultHandle内部持有我们的ByteBuf对象。当我们想获取一个ByteBuf 就优先从stack里面获取,如果没有则尝试从WeakOrderQueue链表中获取,如果WeakOrderQueue还没有则new一个出来。
2.在回收的时候recyclerHandle的recycle方法会把尝试我们的recyclerHandle放入栈中,如果是被stack所属线程回收则直接放入线程对应的stack中,否则获取当前线程对应的Map<Stack, WeakOrderQueue>,并将池对象加入到Stack对应的WeakOrderQueue中(供后续stack.get()时扫描)。
netty对象池的实现机制很大程度上避免了并发和锁竞争带来的性能损耗,是一种空间换时间的实现方式。
来源:CSDN
作者:寒塘渡鹤影..
链接:https://blog.csdn.net/qq_32231495/article/details/103992955