• 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

  • 相关阅读:
    CSU1090 数字转换问题[BFS+素数筛选]
    HDOJ2083 简易版之最短距离
    HOJ11525 Matchsticks
    HDOJ1058 Humble Numbers[DP]
    Sort函数进行升序和降序排列[#include <algorithm>]
    HDOJ1018 求N!的位数[斯特林公式处理阶乘及阶乘位数的问题]
    HDOJ1597 find the nth digit[一元二次方程求解]
    HOJ10641 Equidivisions [BFS]
    HOJ10814 Wooden Sticks[线性DP求最少不递增子序列+结构体排序]
    HOJ12363 Robots on a grid [DP+BFS()]
  • 原文地址:https://www.cnblogs.com/insaneXs/p/13944871.html
Copyright © 2020-2023  润新知