简介
这一篇文章来讲讲Netty的对象池技术。文章中的代码均是依据4.1的版本来分析。
和内存管理的侧重点不同的是,对象池技术主要负责的是针对对象的回收。
换句话说,对象池技术针对的是对象的回收,管理的主体是对象,只不过对象也需要内存空间才能创建,因此在这个过程中,内存只是对象的载体。
而内存管理技术针对的是独立的内存块,管理的主体是对象,但是我们又需要一个对象来表示这个内存块的引用,以便于我们访问,因此在这个过程中,对象其实是内存的载体。
因为这两种技术经常会一起使用,所以在开始后续流程的学习前,还是务必先理清二者的区别。
对象池——Recycler
Recycler 类就是对象池,对象管理的关键逻辑都在这个类上。
Recycler 是一个抽象的泛型类。泛型参数表示实际使用场景下,需要负责管理的对象类型。
虽然这个类被声明为抽象的,但是对象管理的主体逻辑都已经在固定了——Recycler 大部分方法都被声明为final,说明它并不希望子类去修改这些逻辑。而留给子类拓展的仅仅是newObject()
方法,当池中没有缓存的对象时,用来创建新的对象(因为创建对象的逻辑可能需要用户自己定义)。
从属性开始分析
属性分成两个部分:
一、是全局的配置,通常用作在没有设置初始值的情况下提供默认的处置值。这类属性都是类的静态属性。
DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD
默认值是4 * 1024DEFAULT_MAX_CAPACITY_PER_THREAD
默认会使用DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD
做默认值,即 4 * 1024INITIAL_CAPACITY
默认值为DEFAULT_MAX_CAPACITY_PER_THREAD
或是256MAX_SHARED_CAPACITY_FACTOR
默认值为2MAX_DELAYED_QUEUES_PER_THREAD
默认值为CPU个数的两倍LINK_CAPACITY
默认值为16 LINK的大小(LINK是队列中的一个节点,LINK之间互相连接,形成队列,同时LINK内部又是一个数组,可以存放多个对象,数组的大小就由LINK_CAPACITY控制)RATIO
默认值为8
以上这些参数默认值均可以通过特定的系统参数进行调整。
二、是对象的配置,如果在创建Recycler时,构造函数中带了相关的配置,那么这些配置会覆盖默认参数。
maxCapacityPerThread
,对应的默认值就是上文的DEFAULT_MAX_CAPACITY_PER_THREAD
,表示每个线程的最大容量,即表示Stack的最大栈深度(Stack及其作用将在下文介绍)maxSharedCapacityFactor
对应的默认值就是上文的MAX_SHARED_CAPACITY_FACTOR
,,表示每个线程maxCapacityPerThread
与sharedCapacity
的比例关系,即 sharedCapacity = maxCapacity / maxSharedCapacityFactorratioMask
和上文的RATIO
相关,表示回收比例,用来控制回收的频率,避免回收的过快maxDelayedQueuesPerThread
, 和上文MAX_DELAYED_QUEUES_PER_THREAD
相关,表示每个线程允许拥有的最大Queue的数量(Queue及其作用也会在下文详细介绍)
真正影响对象池的配置是这四个相关属性,上文的静态属性只是给这个配置项提供了默认值。
此外,类当中还有一个成员变量FastThreadLocal<Stack<T>> threadLocal
。了解jdk的读者应该知道ThreadLocal
是用来存放线程本地变量的,而FastThreadLocal
和ThreadLocal
作用相同,但是对性能进行了优化。从泛型参数中我们可以看到此时存放的是Stack
类的对象。
已经一个静态变量DELAYED_RECYCLED
,同样是FastThreadLocal,只不过保存的类型的是Map,其中Map的Key是Stack,而Value是WeakOrderQueue。后面我们会了解到这个变量保存了某个线程为其他Stack创建的WeakOrderQueue。
几个内部类及其之间的关联
Recycler的属性还是比较少的,但是内部类却有好几个,分别是:
Stack
——用来存放回收对象WeakOrderQueue
——存放其他线程回收的对象DefaultHandle
——对象句柄
Stack——用来存放回收对象的栈
Stack
是存储回收对象的核心类。当回收对象时,会通过入栈的方式将对象押入Stack中保存(push()
过程)。而申请对象时,会通过出栈的方式将保存的对象弹出给申请者(pop()
过程)。
同时,每个线程都有自己的Stack实例(可以从上文的FastThreadLocal<Statk<T>>
中确定),说明每个线程最终都是各自回收自己创建的对象并保存(注意是最终,其他线程可能参与帮助回收的工作,并暂存到WeakOrderQueue
中过渡)。
这里的Stack并没有直接使用JDK中提供的java.util.Stack
,因为java.util.Stack
不具备这里所需的一些额外特性。而是直接依赖数组重新实现。
来了解下Stack的内部结构:
//关联的对象池 Recycler对象
final Recycler<T> parent;
//栈拥有线程的引用(所属线程)
final WeakReference<Thread> threadRef;
/****由Recycler的相关属性设置******/
//可共享的容量
final AtomicInteger availableSharedCapacity;
//队列的数量
final int maxDelayedQueues;
//栈最大深度
private final int maxCapacity;
//控制回收比例
private final int ratioMask;
//栈底层依赖的数组 存放的是句柄——DefaultHandle,而非直接对象的引用
private DefaultHandle<?>[] elements;
//栈大小
private int size;
//回收计数,配合ratioMask 可以决定此次是否回收
private int handleRecycleCount = -1; // Start with -1 so the first one will be recycled.
/********** WeakOrderQueue形成的链表*****************/
//当前指针,前一个指针;用来决定从哪些WeakOrderQueue中转移对象到Stack中
private WeakOrderQueue cursor, prev; //cursor 记录当前WeakOrderQueue链表的位置 因为链表是头插 所以需要cursor标记
//链表的实际表头
private volatile WeakOrderQueue head; //真正的链表头节点 每次创建新的WeakQueue时 会作为头节点插入链表
从上面的代码中我们可以了解到一下几点信息:
- Stack内部使用数组存放对象句柄(
DefaultHandler
),栈的最大深度即数组的容量,由Recycler的相关属性确定 - 每个Stack都是线程私有的,Stack的拥有线程通过threadRef记录
- Stack内部有一个WeakOrderQueue的链表,除了记录链表的表头(
head
)外,还且记录了链表的当前的游标(cursor
),和有标的前继节点(prev
)
Stack的代码暂时先分析到这,下文会在对象回收和申请的流程中再详细介绍。
DefaultHandle——默认的对象句柄
DefualtHandle是接口Handle
的默认实现,该接口声明了一个方法——void recycle(Object object)
,即在对象发生回收时,由句柄开始发起回收流程。
在早起的Netty版本中,Recycler直接提供了回收的接口,但是这个接口已经被废弃了,取而代之的就是Handler.recycle的接口。这样可以隐藏Stack和Recycler的一些细节。
DefaultHandle是Handle的默认实现,内部结构相对简单。
static final class DefaultHandle<T> implements Handle<T> {
//记录回收的id 和是否被回收的状态
private int lastRecycledId;
private int recycleId;
boolean hasBeenRecycled;
//句柄关联的stack
private Stack<?> stack;
//句柄引用的对象
private Object value;
//构造方法 与stack绑定
DefaultHandle(Stack<?> stack) {
this.stack = stack;
}
//回收动作,对象入栈
@Override
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
//将对象推入栈中
stack.push(this);
}
}
DefaultHandle的代码相对简单,从上面的代码中也可以总结出几点:
- 句柄通过value对象持有对象的引用
- 句柄和Stack对象是相互关联的,Stack分配对象后,对象的句柄就和该Stack绑定了,这样从句柄就知道该对象是哪个Stack分配的,继而也能推断出是哪个线程负责创建的
WeakOrderQueue——线程帮助回收非本线程创建的对象的暂存地
从整体来看的WeakOrderQueue的作用是用来暂存回收的对象的。那么什么样的对象会被WeakOrderQueue先暂存,而不是直接保存在Stack中呢?
答案是如果执行回收的线程不是对象的创建线程(前文已经介绍了句柄知道关联的Stack及线程),那么此次回收将会被暂存到WeakOrderQueue中过度。
这样做的好处是可以减少线程间的竞争,提高吞吐量。
从内部来看,WeakOrderQueue是由Link组成的链表,可以将Link看作是链表中的一个节点。
Link相关代码:
static final class Link extends AtomicInteger {
//DefaultHandle的数组,存放回收的句柄
private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
//记录读索引,剩下的都是未读的部分
private int readIndex;
//指向下一节点,形成链表
Link next;
}
Link链表的表头是一个特殊的结构,主要的作用有两个,一个是在自身被回收时,通过finalize()实现释放操作,另一个是在添加节点时,需要先确认共享空间是否还有剩余,避免超出maxSharedCapacity的限制。
WeakOrderQueue除了上述介绍的两个特殊的属性外,其他属性相对简单。
//哑元节点
static final WeakOrderQueue DUMMY = new WeakOrderQueue();
//头节点
private final Head head;
//尾节点
private Link tail;
// pointer to another queue of delayed items for the same stack
//前文已经介绍过的Stack内部会有WeakOrderQueue形成的链表,就是通过这个next指针串联的
private WeakOrderQueue next;
//关联的线程 这里的thread不是stack的线程,而是weakOrderQueue中的线程
private final WeakReference<Thread> owner;
//ID号
private final int id = ID_GENERATOR.getAndIncrement();
总结几点:
- WeakOrderQueue内部是Link构成的链表,每个Link有一个DefaultHandle的数组,用来保存对象
- WeakOrderQueue之间互相形成链表,表示某个Stack下的所有WeakOrderQueue
相关流程
在了解了Recycler及内部主要类的结构之后,我们再通过Recycler回收及申请流程,加深各个变量和内部类的作用。
对象回收流程
前文提到过,对象的回收流程是从调用DefaultHandle.recycle()
方法开始。我们便以此为入口,来看看对象回收的流程。
- 开始回收后,句柄会首先校验回收的对象即引用的对象,然后由内部关联的Stack通过入栈操作,回收对象,即
stack.push(defaultHandle)
; - 具体的入栈过程根据执行回收动作的线程是否是该stack的拥有者分为
pushNow()
和pushLater()
两个过程 - 如果回收的线程A就是该stack的拥有者,说明是线程A回收自己创建的对象,那么通过
pushNow()
直接将对象回收到Stack内部的数组中保存(当然,也需要考虑ratioMask
和数组的容量,前者用来控制回收的频率,避免回收过快;而后者用来控制回收的最大数量,避免回收过多) - 如果回收的线程A不是该stack的拥有者,说明不是对象的创建线程回收(我们将对象的创建线程先称为B),那么会进入
pushLater()
尝试将对象先暂存到特定的WearOrderQueue中。如果找特定的WeakOrderQueue呢?首先,通过前文介绍的类型为FastThreadLocal的变量DELAYED_RECYCLED
,先获取回收线程A创建的所有的WeakOrderQueue,得到一个Map对象,在通过stack对象去查找线程A是否为该stack创建过WeakOrderQueue。如果没创建,则尝试创建一个WeakOrderQueue(但如果已经线程A创建的WeakOrderQueue已经到达最大数量或者该Stack的最大共享容量已经不够,那么将不会创建新的WeakOrderQueue,也就不会再去回收该对象。此外,对于前者的情况,还会在map中为该Stack关联一个特殊的哑元节点DUMMY,表示不会再尝试创建新WeakOrderQueue)。如果能新建WeakOrderQueue或是已经有WeakOrderQueue,那么会由WeakOrderQueue暂存对象。即将对象保存在WeakOrderQueue内部Link链表的尾节点的数组中。如果尾节点容量已经满了,会新建一个Link节点,并添加到链表的尾部,成为新的tail节点。同理,新的Link节点的创建也需要考虑是否超过最大共享容量avaliableSharedCapacity
,如果超过了,则拒绝创建新的Link节点,也不回收该对象。
对象申请流程
对象的申请流程是从Recycler.get()
开始的,即从对象池中获取对象。流程如下:
- 获取线程关联的Stack,前文已经介绍过了每个线程都有自己的Stack来保存对象。如果该线程还没有Stack,则通过
initValue()
创建一个Stack。Recycler的相关属性值会被用来创建Stack。 - 从Stack中尝试弹出对象(
stack.pop()
),如果此时能够弹出对象,说明该Stack之前回收过对象。如果没有回收到的对象,则会创建一个新对象。 - 创建新对象分为两步,第一创建由
stack.newHandle()
创建对象句柄,第二,由要通过newObject(handle)
方法创建对象,这是一个Recycler
的抽象方法,由具体的对象池子类根据管理对象的不用自行实现。得到的DefaultHandle会持有对象的引用。
新对象的创建过程还是比较简单的,主要还是理解从Stack中弹出对象的过程。我们已经了解到回收的对象可能存放在Stack内部的数组和WeakOrderQueue中Link的数组两个地方,其实弹出也正是从这里找对象,并返回。
首先,出栈过程会先从栈中获取元素,如果此时栈中没有元素,那么会从WeakOrderQueue中将暂存的元素移动到栈中。然后再从栈的尾部获取元素。
Recycler相关类之间的关系
简单将Recycler内部的类之间的关系画了一个图。帮助读者理解不同线程下给个类之间的关联。
思考
Netty其实已经提供了一个非常强大的对象池框架,利用这套框架我们也可以很容易的实现自己的对象池需求,譬如连接池等。
详细的源码注释可以见为的Github。