Netty的对象池

简介

这一篇文章来讲讲Netty的对象池技术。文章中的代码均是依据4.1的版本来分析。
和内存管理的侧重点不同的是,对象池技术主要负责的是针对对象的回收。
换句话说,对象池技术针对的是对象的回收,管理的主体是对象,只不过对象也需要内存空间才能创建,因此在这个过程中,内存只是对象的载体。
而内存管理技术针对的是独立的内存块,管理的主体是对象,但是我们又需要一个对象来表示这个内存块的引用,以便于我们访问,因此在这个过程中,对象其实是内存的载体。
因为这两种技术经常会一起使用,所以在开始后续流程的学习前,还是务必先理清二者的区别。

对象池——Recycler

Recycler 类就是对象池,对象管理的关键逻辑都在这个类上。
Recycler 是一个抽象的泛型类。泛型参数表示实际使用场景下,需要负责管理的对象类型。
虽然这个类被声明为抽象的,但是对象管理的主体逻辑都已经在固定了——Recycler 大部分方法都被声明为final,说明它并不希望子类去修改这些逻辑。而留给子类拓展的仅仅是newObject()方法,当池中没有缓存的对象时,用来创建新的对象(因为创建对象的逻辑可能需要用户自己定义)。

从属性开始分析

属性分成两个部分:
一、是全局的配置,通常用作在没有设置初始值的情况下提供默认的处置值。这类属性都是类的静态属性。

  • DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD默认值是4 * 1024
  • DEFAULT_MAX_CAPACITY_PER_THREAD默认会使用DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD做默认值,即 4 * 1024
  • INITIAL_CAPACITY默认值为DEFAULT_MAX_CAPACITY_PER_THREAD或是256
  • MAX_SHARED_CAPACITY_FACTOR默认值为2
  • MAX_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,,表示每个线程maxCapacityPerThreadsharedCapacity的比例关系,即 sharedCapacity = maxCapacity / maxSharedCapacityFactor
  • ratioMask 和上文的RATIO相关,表示回收比例,用来控制回收的频率,避免回收的过快
  • maxDelayedQueuesPerThread, 和上文MAX_DELAYED_QUEUES_PER_THREAD相关,表示每个线程允许拥有的最大Queue的数量(Queue及其作用也会在下文详细介绍)

真正影响对象池的配置是这四个相关属性,上文的静态属性只是给这个配置项提供了默认值。

此外,类当中还有一个成员变量FastThreadLocal<Stack<T>> threadLocal。了解jdk的读者应该知道ThreadLocal是用来存放线程本地变量的,而FastThreadLocalThreadLocal作用相同,但是对性能进行了优化。从泛型参数中我们可以看到此时存放的是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()方法开始。我们便以此为入口,来看看对象回收的流程。

  1. 开始回收后,句柄会首先校验回收的对象即引用的对象,然后由内部关联的Stack通过入栈操作,回收对象,即stack.push(defaultHandle);
  2. 具体的入栈过程根据执行回收动作的线程是否是该stack的拥有者分为pushNow()pushLater()两个过程
  3. 如果回收的线程A就是该stack的拥有者,说明是线程A回收自己创建的对象,那么通过pushNow()直接将对象回收到Stack内部的数组中保存(当然,也需要考虑ratioMask和数组的容量,前者用来控制回收的频率,避免回收过快;而后者用来控制回收的最大数量,避免回收过多)
  4. 如果回收的线程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()开始的,即从对象池中获取对象。流程如下:

  1. 获取线程关联的Stack,前文已经介绍过了每个线程都有自己的Stack来保存对象。如果该线程还没有Stack,则通过initValue()创建一个Stack。Recycler的相关属性值会被用来创建Stack。
  2. 从Stack中尝试弹出对象(stack.pop()),如果此时能够弹出对象,说明该Stack之前回收过对象。如果没有回收到的对象,则会创建一个新对象。
  3. 创建新对象分为两步,第一创建由stack.newHandle()创建对象句柄,第二,由要通过newObject(handle)方法创建对象,这是一个Recycler的抽象方法,由具体的对象池子类根据管理对象的不用自行实现。得到的DefaultHandle会持有对象的引用。

新对象的创建过程还是比较简单的,主要还是理解从Stack中弹出对象的过程。我们已经了解到回收的对象可能存放在Stack内部的数组和WeakOrderQueue中Link的数组两个地方,其实弹出也正是从这里找对象,并返回。
首先,出栈过程会先从栈中获取元素,如果此时栈中没有元素,那么会从WeakOrderQueue中将暂存的元素移动到栈中。然后再从栈的尾部获取元素。

Recycler相关类之间的关系

简单将Recycler内部的类之间的关系画了一个图。帮助读者理解不同线程下给个类之间的关联。

思考

Netty其实已经提供了一个非常强大的对象池框架,利用这套框架我们也可以很容易的实现自己的对象池需求,譬如连接池等。
详细的源码注释可以见为的Github

原文地址:https://www.cnblogs.com/insaneXs/p/13944871.html