Recycler源码实现&思考

一、作用&思想

1.1 作用

  1. 当出现频繁创建对象,销毁对象的时候,为了提高性能,可以将其缓存,减少对象创建的频次;

1.2 思想

  1. 并发、串行、无锁化思想贯穿其中
  2. 空间换时间思想,提高运行效率
  3. 规则约束,避免空间被无限增大,出现意想不到的情况;比如:长度约束;最终可见性(而不是实时可见性)约束
  4. 谁创建谁回收重利用,规则清晰

二、源码中使用场景

  1. PooledByteBuf对象
  2. ChannelOutboundBuffer中的Entry对象

后面以PooledByteBuf为例,进行讲解Recycler的运行原理

三、简单用例

当前是低版本的用例,4.1.38版本;高版本的话, 把Recycler替换成ObjectPool(语义上更加清晰)

java 复制代码
static class Student {
    private static  final Recycler<Student> recycler = new Recycler<Student>() {
        @Override
        protected Student newObject(Handle<Student> handle) {
            return new Student(handle);
        }
    };
     private final Handle<Student> handle;
     public String name = "zhangsan";
     public int age = 1;

     public Student(Handle<Student> handle) {
         this.handle = handle;
     }

     public void recyle() {
         handle.recycle(this);
     }

     public void setName(String name) {
         this.name = name;
     }

     public static Student getInstance() {
         return recycler.get();
     }
 }

四、4.1.38版本低版本

注:规定一些术语定义

  1. 创建线程:创建PooledByteBuf对象的线程,PooledBytebuf.getInstance()
  2. 回收线程:回收该PooledByteBuf对象的线程,bytebuf.release()
  3. 该定义源引自大佬文章[1]

4.1 整体类图结构

4.2 整体流程

4.2.1 释放对象流程

4.2.1.1 释放对象的流程图
4.2.1.2 源码解析
io.netty.util.Recycler.Stack#push
  1. 比较创建线程和当前线程是否一致;
  2. 如果一致,说明同一线程,则直接调用pushNow,放到stack中;(比较简单,就不展开了)
  3. 如果不一致,则调用pushLater;(不同线程如何回收才是重点
java 复制代码
void push(DefaultHandle<?> item) {
    Thread currentThread = Thread.currentThread();
    if (threadRef.get() == currentThread) {
        // The current Thread is the thread that belongs to the Stack, we can try to push the object now.
        pushNow(item);
    } else {
        // The current Thread is not the one that belongs to the Stack
        // (or the Thread that belonged to the Stack was collected already), we need to signal that the push
        // happens later.
        pushLater(item, currentThread);
    }
}
io.netty.util.Recycler.Stack#pushLater
  1. DELAYED_RECYCLED是一个FastThreadLocal,也就是说每个线程都有一个Map映射表,用于记录Stack和WeakOrderQueue之间的关系;
  2. 从delayedRecycled获取当前stack对应的WeakOrderQueue,准备将item放入这个WeakOrderQueue
  3. 如果queue==null,且如果说明还没有初始化,如果还大于maxDelayQueues,说明超限了,不能放了;否则就开始创建;创建是一个重点,(敲黑板)
  4. 然后将这个创建的WeakOrderQueue加入到delayedRecycled中
  5. 最后将item加入到queue中
java 复制代码
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>();
    }
};

private void pushLater(DefaultHandle<?> item, Thread thread) {
    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;
        }
        if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {
            // drop object
            return;
        }
        delayedRecycled.put(this, queue);
    } else if (queue == WeakOrderQueue.DUMMY) {
        // drop object
        return;
    }

    queue.add(item);
}
io.netty.util.Recycler.WeakOrderQueue#allocate
  1. 如果availableSharedCapacity-LINK_CAPACITY>0 ,说明空间还够分配,否则就不分配;
  2. 分配时,先创建一个WeakOrderQueue,同时设置当前stack的head指针指向这个queue;那么最终形成的结构图如下图所示
arduino 复制代码
static WeakOrderQueue allocate(Stack<?> stack, Thread thread) {
    return Head.reserveSpace(stack.availableSharedCapacity, LINK_CAPACITY)
            ? newQueue(stack, thread) : null;
}

static WeakOrderQueue newQueue(Stack<?> stack, Thread thread) {
    final WeakOrderQueue queue = new WeakOrderQueue(stack, thread);
    // Done outside of the constructor to ensure WeakOrderQueue.this does not escape the constructor and so
    // may be accessed while its still constructed.
    stack.setHead(queue);

    return queue;
}

最终形成的结构图:

每个线程持有该stack里面的一个WeakOrderQueue,通过这个Queue,将其他线程回收的对象放到并行无阻塞的放入到WeakOrderQueue中;

io.netty.util.Recycler.WeakOrderQueue#add
  1. 如果writeIndex==LINK_CAPACITY的话,说明当前的link已经满了;需要申请新空间;此时如果空间还不足以申请LINK_CAPACITY大小,直接丢弃即可;否则新创建一个Link,将tail指针后移,指向这个Link对象;
  2. 将这个元素加入到tail的elements中
  3. 长度加1
java 复制代码
void add(DefaultHandle<?> handle) {
    handle.lastRecycledId = id;

    Link tail = this.tail;
    int writeIndex;
    if ((writeIndex = tail.get()) == LINK_CAPACITY) {
        if (!head.reserveSpace(LINK_CAPACITY)) {
            // Drop it.
            return;
        }
        this.tail = tail = tail.next = new Link();

        writeIndex = tail.get();
    }
    tail.elements[writeIndex] = handle;
    handle.stack = null;
    tail.lazySet(writeIndex + 1);
}

所以对象被release后,形成的最终结构图如下所示:

4.2.2 对象申请流程

4.2.2.1 对象申请的流程图
1、同线程中获取回收线程流程
2、从其他线程中获取回收对象流程
4.2.2.2 源码解析
io.netty.util.Recycler#get
  1. 从threadlocal获取当前的stack对象;
  2. 从stack中弹出一个元素,如果为空,则说明没有可重用的对象,新建一个Handle对象;
  3. 返回handle中包装的value对象
ini 复制代码
public final T get() {
    if (maxCapacityPerThread == 0) {
        return newObject((Handle<T>) NOOP_HANDLE);
    }
    Stack<T> stack = threadLocal.get();
    DefaultHandle<T> handle = stack.pop();
    if (handle == null) {
        handle = stack.newHandle();
        handle.value = newObject(handle);
    }
    return (T) handle.value;
}
io.netty.util.Recycler.Stack#pop
  1. 如果当前stack中size=0,说明可重用对象数量=0
  2. 执行scavenge方法,从其他线程扫描
  3. 然后从elements中获取可重用元素,
  4. 同时将elements[size]=null, 这一块很精巧,这样就能保证从elements里面获取一定是有值(无论是null还是非null),因为从pop返回以后,在get方法里面会有一个==null的判断,根据非null条件来判断是否需要创建对象;
ini 复制代码
DefaultHandle<T> pop() {
    int size = this.size;
    if (size == 0) {
        if (!scavenge()) {
            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;
}

io.netty.util.Recycler.Stack#scavenge

  1. 如果能扫描到,直接返回true;
  2. 如果没有扫描到,将prev置为null, cursor置为head;(cursor代表当前节点,prev代表当前节点的前一个节点,head代表头结点, 遍历是否有重用对象是从cursor逐步向后移动)
typescript 复制代码
boolean scavenge() {
    // continue an existing scavenge, if any
    if (scavengeSome()) {
        return true;
    }

    // reset our scavenge cursor
    prev = null;
    cursor = head;
    return false;
}
io.netty.util.Recycler.Stack#scavengeSome

重头戏,逐行解析

  1. 从4.2.1.2章节看到的最终结构图中了解到,当回收完成后,一个Stack持有WeakOrderQueue的链表,每一个WeakOrderQueue代表一个其他线程帮创建线程回收的对象;
  2. head 代表头指针
  3. cursor 代表当前待操作的指针; 疑问:为什么cursor和head不能合二为一呢?
  4. prev 代表当前指针的前一个位置
  5. owner是一个WeakReference,一旦为null,则说明被GC了,那这个线程里面的所有被回收的线程都需要被移走;
java 复制代码
boolean scavengeSome() {
    WeakOrderQueue prev;
    // 获取当前指针 
    WeakOrderQueue cursor = this.cursor;
    // 指针为空,说明可能是第一次执行
    if (cursor == null) {
        prev = null;
        cursor = head;
        // 如果赋值head后,也为空,说明当前没有元素,不用在扫描了;
        if (cursor == null) {
            return false;
        }
    } else {
        prev = this.prev;
    }

    boolean success = false;
    do {
        // 从cursor执行的WeakOrderQueue中进行转移元素,重点!!!
        if (cursor.transfer(this)) {
            success = true;
            break;
        }
        WeakOrderQueue next = cursor.next;
        // 说明被GC掉了,那这个线程被干掉了,里面的数据也需要全量被迁移走;
        if (cursor.owner.get() == null) {
            if (cursor.hasFinalData()) {
                for (;;) {
                    if (cursor.transfer(this)) {
                        success = true;
                    } else {
                        break;
                    }
                }
            }

            if (prev != null) {
                prev.setNext(next);
            }
        } else {
            prev = cursor;
        }
        // 如果在当前cursor中没有找到, prev、cursor后移一位,

        cursor = next;
    // 直至将链表中元素遍历完成,或者找到执行元素
    } while (cursor != null && !success);
    
    // prev、cursor进行赋值回Stack对象;
    this.prev = prev;
    this.cursor = cursor;
    return success;
}
io.netty.util.Recycler.WeakOrderQueue#transfer

这个方法也是贼长,核心点

  1. 如果是最后一个LINK,且readIndex = LINK_CAPACITY,则返回false,同时回收空间
  2. 否则获取一个LINK中所有元素,将其迁移到Stack的elements中;
  3. 如果stack的elements长度不够,则需要扩容;
ini 复制代码
boolean transfer(Stack<?> dst) {
    // 从数据节点(LINK)开始扫描,
    Link head = this.head.link;
    if (head == null) {
        return false;
    }
    // 如果当前节点已经读完了
    if (head.readIndex == LINK_CAPACITY) {
        // 且没有下一个LINK了,则返回false;
        if (head.next == null) {
            return false;
        }
        // 指针后移一位
        this.head.link = head = head.next;
        // 回收这一块由回收线程占用的空间
        this.head.reclaimSpace(LINK_CAPACITY);
    }

    // 需要读的起点(有回收对象的起点)
    final int srcStart = head.readIndex;
    // 有回收对象的终点(writeIndex)
    int srcEnd = head.get();
    // 需要迁移的数据的个数;
    final int srcSize = srcEnd - srcStart;
    // 如果没有数据,异常情况,返回false
    if (srcSize == 0) {
        return false;
    }

    // 当前Stack中元素的个数
    final int dstSize = dst.size;
    // 一旦迁移到Stack,那么Stack最少要达到expectedCapacity
    final int expectedCapacity = dstSize + srcSize;

    // 如果expectedCapacity 大于 Stack的elements的阈值,则需要扩容;
    if (expectedCapacity > dst.elements.length) {
        // 扩容大小是之前的2倍;
        final int actualCapacity = dst.increaseCapacity(expectedCapacity);
        // 最多查看迁移的个数;因为有可能Stack的elements可能没有那么多空间了;
        srcEnd = min(srcStart + actualCapacity - dstSize, srcEnd);
    }
    // 如果起点和终点不一样,说明有需要回收的元素
    if (srcStart != srcEnd) {
        final DefaultHandle[] srcElems = head.elements;
        final DefaultHandle[] dstElems = dst.elements;
        int newDstSize = dstSize;
        // 遍历Link中的元素,然后将其放到Stack的elements中
        for (int i = srcStart; i < srcEnd; i++) {
            DefaultHandle element = srcElems[i];
            // 代表已经被完全回收了,如果他们两个不一样,说明该element被其他回收线程回收了,还没有最终回到创建线程;(也就是还不能使用,需要这样一次迁移)
            if (element.recycleId == 0) {
                element.recycleId = element.lastRecycledId;
            } else if (element.recycleId != element.lastRecycledId) {
                throw new IllegalStateException("recycled already");
            }
            srcElems[i] = null;
            // 这里还有一次判断,因为可能会有大量的对象会创建;
            // 所以默认1/8的比例进行缓存对象;
            if (dst.dropHandle(element)) {
                // Drop the object. 如果被丢弃了,就需要继续遍历
                continue;
            }
            // 将element中的stack指针指向创建线程的stack
            element.stack = dst;
            // stack中的元素++
            dstElems[newDstSize ++] = element;
        }
        
        // 如果刚刚好达到一个LINK的最大值,也就是占满了一个LINK,而且这个LINK刚刚被回收掉了,那么这个LINK就可以被回收掉;
        if (srcEnd == LINK_CAPACITY && head.next != null) {
            // Add capacity back as the Link is GCed.
            this.head.reclaimSpace(LINK_CAPACITY);
            this.head.link = head.next;
        }
        // 将readIndex指向移动完成的元素位置;
        head.readIndex = srcEnd;
        // 如果移动完成以后,新老的stack的elements队列的长度是一致的,说明没有可重用对象,返回false
        if (dst.size == newDstSize) {
            return false;
        }
        dst.size = newDstSize;
        return true;
    } else {
        // The destination stack is full already.
        return false;
    }
}

问题集

为什么head和cursor两个变量合二为一,每次从head开始遍历?

  1. head是一直在移动的,每多一个线程,head就会往前移动一次;从head开始扫描,可能造成对象使用不均匀,WeakOrderQueue链表后面的被回收的元素无法被使用到,那对应的回收线程回收对象的数量一旦达到阈值,一旦在有对象被release,但是因为达到了回收阈值,所以这些对象都会被丢弃;这样会导致有些线程无法被使用,资源利用不均匀;
  2. 但从cursor继续往下扫描有一个问题点,就是cursor一直扫描到最后,发现没有元素了,而不会从head在扫描一遍,就会直接返回false;那此次就无法重利用;就会导致从(head, cursor)区间内新回收的对象就无法得到重用;我理解设计者这样做的考虑是:利用一次无利用(一次创建对象),来避免从head的多次扫描,从性能、代码复杂度做了均衡考虑

五、高版本(4.1.112)Recycler的变化

变化一:整体架构设计,结构更加简洁

  1. 之前用Stack、WeakOrderQueue来实现并发、串行无锁化
  2. 现在变成了使用MpscQueue完成并发串行无锁化的设计;
  3. 为了和以前接口兼容,所以需要保留之前的DefaultHandle等接口

变化二:封装更加合理

  1. 语义更加清晰,通过开放ObjectPool来表示当前对象是放在一个对象池中
  2. 将Recycler进一步封装
  3. 同时又封装一层Handle,且不失兼容性;

MpscQueue的源码讲解(后续讲解)

参看网址:

【参看一】抓到Netty一个隐藏很深的内存泄露Bug | 详解Recycler对象池的精妙设计与实现

相关推荐
daidaidaiyu3 天前
一文学习和实践 当下互联网安全的基石 - TLS 和 SSL
java·netty
enjoy编程4 天前
Spring boot 4 探究netty的关键知识点
spring boot·设计模式·reactor·netty·多线程
ps酷教程7 天前
HttpData
http·netty
ps酷教程12 天前
ChunkedWriteHandler源码浅析
java·netty·分块传输
奕辰杰15 天前
Netty私人学习笔记
笔记·学习·netty·网络通信·nio
ps酷教程17 天前
HttpObjectDecoder源码浅析
java·netty·httpaggregator
魔芋红茶22 天前
Netty 简易指南
java·开发语言·netty
ps酷教程22 天前
HttpAggregator源码浅析
netty·httpaggregator
9527出列25 天前
Netty实战--使用netty构建WebSocket服务
websocket·网络协议·netty