Netty4.1源码阅读——前传(Recycler)

529 阅读7分钟

前言

最近在看netty源码,在阅读netty的过程中,深深的感到系统实现比较复杂,而源码不只是了解实现逻辑,还要从中挖掘出一些有价值的东西,学习到一些技术能够运用到实际的生产环境中。在探究netty高性能的原因途中,发现了一些代码觉得挺有用,所以记录下来以供参考。

正文

简介

由于java在运行过程中,即使有GC可以让我们无需关注内存释放,但频繁的创建和销毁对象也会导致GC性能下降,所以netty编写了轻量级的对象池来辅助管理。

Recycler原理

Recycler将对象进行循环使用,当一个对象使用完毕后,利用Recycler进行回收,当需要一个对象的时候将从Recycler取出一个对象,如果没有任何可以用的对象则new一个对象。

Recycler的三个主要方法如下:

column1column2
T get()获取对象
newObject()需要自己实现的方法,创建一个新的对象
public final boolean recycle(T o, Handle handle)回收一个对象
/**
     * 一个空的回收方法,表示不需要回收的对象,主要用于占位功能
     * 当Stack的默认容量为0的时候使用 这时候无法使用回收功能
     */
    @SuppressWarnings("rawtypes")
    private static final Handle NOOP_HANDLE = new Handle() {
        @Override
        public void recycle(Object object) {
            // NOOP
        }
    };
    /**
     * 当前线程ID和 WeakOrderQueueID
     */
    private static final AtomicInteger ID_GENERATOR = new AtomicInteger(Integer.MIN_VALUE);
    private static final int OWN_THREAD_ID = ID_GENERATOR.getAndIncrement();
    /**
     * Stack默认的最大初始最大容量:4kb
     */
    private static final int DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD = 4 * 1024;
    /**
     * Stack的默认容量
     * io.netty.recycler.maxCapacityPerThread和io.netty.recycler.maxCapacity参数配置
     * 当参数小于0的时候设置默认上面4kb,等于0的时候表示禁用
     */
    private static final int DEFAULT_MAX_CAPACITY_PER_THREAD;
    /**
     * Stack默认的初始容量,下面代表表示默认为256k
     * 自动根据需要进行扩容,直到达到MAX_CAPACITY_PER_THREAD
     */
    private static final int INITIAL_CAPACITY;
    /**
     * 最大共享容量因子:maxCapacity / maxSharedCapacityFactor,默认为2
     */
    private static final int MAX_SHARED_CAPACITY_FACTOR;
    /**
     * 每个线程可拥有WeakOrderQueue数量
     * 通过io.netty.recycler.maxDelayedQueuesPerThread数量
     * 默认为2*cpu核数
     */
    private static final int MAX_DELAYED_QUEUES_PER_THREAD;
    /**
     * 每个WeakOrderQueue内部Link中Handle的数量
     * io.netty.recycler.linkCapacity配置 默认为16
     * Link组成一个Link链表
     */
    private static final int LINK_CAPACITY;
    /**
     * 回收因子
     * io.netty.recycler.ratio配置 默认为8
     */
    private static final int RATIO;
    private static final int DELAYED_QUEUE_RATIO;

以上是Recycler的静态参数列表,比较容易理解

从以上参数中可以看出来,Recycler内部有三个主要参数:

  • Stack:不是JAVA内部的Stack,但是功能差不多,用于存储当前线程回收的对象
  • WeakOrderQueue:用于存储其他线程回收到当前线程的对象
  • Link:WeakOrderQueue存储对象的元素,一个Link默认存储16个对象,超过了创建一个新的Link
  • DefaultHandle:默认的handler实现

    /**
     * 每个线程包含一个Stack 存储在ThreadLocal中
     * 根据Recycler的构造函数初始化Stack
     */
    private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {
        @Override
        protected Stack<T> initialValue() {
            return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor,
                    interval, maxDelayedQueuesPerThread, delayedQueueInterval);
        }

        @Override
        protected void onRemoval(Stack<T> value) {
            // Let us remove the WeakOrderQueue from the WeakHashMap directly if its safe to remove some overhead
            if (value.threadRef.get() == Thread.currentThread()) {
               if (DELAYED_RECYCLED.isSet()) {
                   DELAYED_RECYCLED.get().remove(value);
               }
            }
        }
    };

 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>();
        }
    };

都是放到封装过的ThreadLocal里面

Recyclern内部图

DDF0E4A420FBDEB078B6A99E477DBCE2.png

Recycler使用了Stack来存储同一个类型的Handle,这个图表达了上面代码的作用用于保存线程共享对象,当非当前线程去回收一个对象的时候,就会使用到共享对象。

DefaultHandle实现

首先看一下DefaultHandle内部实现

 private static final class DefaultHandle<T> implements Handle<T> {
        int lastRecycledId;
        int recycleId;

        boolean hasBeenRecycled;

        Stack<?> stack;
        Object value;

        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<?> stack = this.stack;
            if (lastRecycledId != recycleId || stack == null) {
                throw new IllegalStateException("recycled already");
            }

            stack.push(this);
        }
    }

DefaultHandle将需要回收的对象给包装,使用lastRecycledId和recycleId来组织重复回收,做的操作仅仅是放入stack

Stack内部实现

DefaultHandle实现很简单,把自己放入了Stack,现在看一下Stack内部有哪些元素

 /**
         * 保存了Stack所属的线程
         * 按照对象回收策略 如果对象没有被强引用了就可以被回收了 这里使用弱引用是因为
         * 如果用户持有Handle对象一直不释放 Stack又持有Handle的强引用 使用弱引用可以让JVM释放该线程对象
         */
        final WeakReference<Thread> threadRef;
        /**
         * 可共享内存大小 按照之前配置的共享因子计算 默认2kb
         * 表示其他线程可回收当前线程的最大容量,这也表示当前线程可以被回收最大容量为(4+2)kb
         */
        final AtomicInteger availableSharedCapacity;
        private final int maxDelayedQueues;

        private final int maxCapacity;
        private final int interval;
        private final int delayedQueueInterval;
        DefaultHandle<?>[] elements;
        int size;
        private int handleRecycleCount;
        private WeakOrderQueue cursor, prev;
	/**
        * 线程B回收线程A创建的对象时,线程B会为线程A的Stack对象创建一个WeakOrderQueue对象,
        * 这个WeakOrderQueue指向head,用于后续线程A对对象的查找操作
	**/
        private volatile WeakOrderQueue head;

使用了elements数组来存储Handle

DefaultHandle<T> pop() {
            int size = this.size;
            if (size == 0) {
                if (!scavenge()) {
                    return null;
                }
		//
                size = this.size;
                if (size <= 0) {
                    // double check, avoid races
                    return null;
                }
            }
            size --;
            DefaultHandle ret = elements[size];
            elements[size] = null;
            this.size = size;

            if (ret.lastRecycledId != ret.recycleId) {
                throw new IllegalStateException("recycled multiple times");
            }
            ret.recycleId = 0;
            ret.lastRecycledId = 0;
            return ret;
        }

取出一个对象,注意第一个if后面重新给size赋值了一次,因为在此过程中调用了scavenge,可能有其他线程handle传递,所以要保证size是一致的。其余的过程就是对数组的操作

image.png

当前线程对象池没有对象的时候,可以从其他线程的保存的对象中拿出来,核心是调用了WeakOrderQueue的transfer方法

 void push(DefaultHandle<?> item) {
            Thread currentThread = Thread.currentThread();
            if (threadRef.get() == currentThread) {
                pushNow(item);
            } else {
                pushLater(item, currentThread);
            }
        }

 private void pushNow(DefaultHandle<?> item) {
            if ((item.recycleId | item.lastRecycledId) != 0) {
                throw new IllegalStateException("recycled already");
            }
            item.recycleId = item.lastRecycledId = OWN_THREAD_ID;

            int size = this.size;
            if (size >= maxCapacity || dropHandle(item)) {
                // Hit the maximum capacity or should drop - drop the possibly youngest object.
                return;
            }
            //扩容
            if (size == elements.length) {
                elements = Arrays.copyOf(elements, min(size << 1, maxCapacity));
            }

            elements[size] = item;
            this.size = size + 1;
        }

如果是当前线程,调用pushNow方法,将对象放入数组中,需要注意容量来选择扩容。

/**
         * 从map中取出queue或者新建一个,前提是数量合法
         */
        private void pushLater(DefaultHandle<?> item, Thread thread) {
            //不支持跨线程回收
            if (maxDelayedQueues == 0) {
                return;
            }
            
            Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
            WeakOrderQueue queue = delayedRecycled.get(this);
            if (queue == null) {
                if (delayedRecycled.size() >= maxDelayedQueues) {
                    delayedRecycled.put(this, WeakOrderQueue.DUMMY);
                    return;
                }
                // Check if we already reached the maximum number of delayed queues and if we can allocate at all.
                if ((queue = newWeakOrderQueue(thread)) == null) {
                    return;
                }
                delayedRecycled.put(this, queue);
            } else if (queue == WeakOrderQueue.DUMMY) {
                return;
            }

            queue.add(item);
        }

如果不是当先线程,就要去map里面找到属于该线程的queue放入,需要判断是否超过最大值

FD9B78B111691E442A8C9CD7C7714F95.png

每当一个线程回收对象的时候,就会成为Stack内部的head,为的是将其他线程的Queue与Stack关联。

private WeakOrderQueue cursor, prev;

而cursor代表Queue形成的练表中,当前寻找的Queue,prev表示cursor的上一个

Stack介绍完了,底层维护了一个Handle类型的数组,如果是当前线程就操作这个数组,操作方式和ArrayList类似,同时还支持去其他线程的queue里面拿对象

WeakOrderQueue实现

image.png

WeakOrderQueue继承了WeakReference,具体JAVA三种引用可以百度查询。

List元素内部也是包含Handle数组,并且自身是AtomicInteger就可以用来计数。

在添加新queue的时候,如果超出了最大队列数量,就放入这个DUMMY 不做任何操作。

image.png

Head是Queue的头节点,reclaimAllSpaceAndUnlink表示回收所有空间并且取消链接,因为防止GC的裙带关系

image.png

创建一个新的Link,需要检测空间是否足够,不然就返回null

image.png

看注释,构造时候将tail赋值给了head.link

image.png

add方法,如果一个link内部数组满了,就需要创建下一个link来链接,需要判断有没有足够的空间来创建,没有就直接丢弃这个handle

 boolean transfer(Stack<?> dst) {
            //如果head为空表示这个queue没有handle可以用
            Link head = this.head.link;
            if (head == null) {
                return false;
            }

            //表示这个link已经读完了 需要读下一个link 没有就返回false
            if (head.readIndex == LINK_CAPACITY) {
                if (head.next == null) {
                    return false;
                }
                head = head.next;
                this.head.relink(head);
            }

            //检查当先可读索引和当前容量 相等表示无法读了
            final int srcStart = head.readIndex;
            int srcEnd = head.get();
            final int srcSize = srcEnd - srcStart;
            if (srcSize == 0) {
                return false;
            }

            final int dstSize = dst.size;
            final int expectedCapacity = dstSize + srcSize;

            if (expectedCapacity > dst.elements.length) {
                final int actualCapacity = dst.increaseCapacity(expectedCapacity);
                srcEnd = min(srcStart + actualCapacity - dstSize, srcEnd);
            }

            //重新计算了srcEnd 需要再判断
            if (srcStart != srcEnd) {
                final DefaultHandle[] srcElems = head.elements;
                final DefaultHandle[] dstElems = dst.elements;
                //stack的索引 从这个索引开始跟新
                int newDstSize = dstSize;
                for (int i = srcStart; i < srcEnd; i++) {
                    DefaultHandle<?> element = srcElems[i];
                    if (element.recycleId == 0) {
                        element.recycleId = element.lastRecycledId;
                    } else if (element.recycleId != element.lastRecycledId) {
                        throw new IllegalStateException("recycled already");
                    }
                    //删除link的元素 将这个元素放入Stack中
                    srcElems[i] = null;

                    if (dst.dropHandle(element)) {
                        continue;
                    }
                    element.stack = dst;
                    dstElems[newDstSize ++] = element;
                }

                if (srcEnd == LINK_CAPACITY && head.next != null) {
                    // Add capacity back as the Link is GCed.
                    this.head.relink(head.next);
                }

                head.readIndex = srcEnd;
                if (dst.size == newDstSize) {
                    return false;
                }
                dst.size = newDstSize;
                return true;
            } else {
                return false;
            }
        }
    }

transfer方法的参数是Stack,表示要将这个queue中的对象转换到Stack里面,前面一段注释是对一些条件的判断,比如link是否为空或者是否还有值可读。当一切准备就绪的时候,对数组进行拷贝,将queue内部的对象复制到Stack并且删除


Netty 实现了轻量的对象池,使用了threadLocal,避免了多线程下取数据时可能出现的线程安全问题。同时,为了实现多线程回收同一个实例,让每个线程对应一个队列,队列链接在 Stack 对象上形成链表,使用++软引用map++++软引用线程++也避免了内存泄漏,解决了多线程回收时的安全问题