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对象池的精妙设计与实现

相关推荐
异常君1 天前
一文吃透 Netty 处理粘包拆包的核心原理与实践
java·后端·netty
猫吻鱼2 天前
【Netty4核心原理】【全系列文章目录】
netty
用户90555842148053 天前
AdaptiveRecvByteBuAllocator 源码分析
netty
菜菜的后端私房菜4 天前
深入剖析 Netty 中的 NioEventLoopGroup:架构与实现
java·后端·netty
码熔burning7 天前
【Netty篇】Channel 详解
netty·nio·channel
Pitayafruit14 天前
📌 Java 工程师进阶必备:Spring Boot 3 + Netty 构建高并发即时通讯服务
spring boot·后端·netty
猫吻鱼16 天前
【Netty4核心原理④】【简单实现 Tomcat 和 RPC框架功能】
netty
陌路物是人非20 天前
SpringBoot + Netty + Vue + WebSocket实现在线聊天
vue.js·spring boot·websocket·netty
可乐加.糖21 天前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信
东阳马生架构22 天前
Netty源码—10.Netty工具之时间轮二
netty·时间轮