Netty精粹之轻量级内存池技术实现原理与应用

在Netty中,一般会有多个IO线程独立工做,基于NioEventLoop的实现,每一个IO线程负责轮询单独的Selector实例来检索IO事件,当IO事件来临的时候,IO线程开始处理IO事件。最多见的IO事件即读写事件,那么这个时候就会涉及到IO线程对数据的读写问题,具体到NIO方面即从内核缓冲区读取数据到用户缓冲区或者从用户缓冲区将数据写到内核缓冲区。NIO提供了两种Buffer做为缓冲区,即DirectBuffer和HeapBuffer。这篇文章主要在介绍两种缓冲区的基础之上再介绍Netty基于ThreadLocal的内存池技术的实现原理与应用,并给出一个简单维度的测试数据。java


DirectBuffer和HeapBuffer数组

DirectBuffer顾名思义是分配在直接内存(Direct Memory)上面的内存区域,直接内存不是JVM Runtime数据区的一部分,也不是JAVA虚拟机规范中定义的内存区域,可是这部份内存也被频繁的使用。在JDK1.4版本开始NIO引入的Channel与Buffer的IO方式使得咱们可使用native接口来在直接内存上分配内存,并用JVM堆内存上的一个引用来进行操做,当JVM堆内存上的引用被回收以后,这块直接内存才会被操做系统回收。HeapBuffer即分配在JVM堆内存区域的缓冲区,咱们能够简单理解为HeapBuffer就是byte[]数组的一种封装形式。多线程

基于HeapBuffer的IO写流程一般是先要在直接内存上分配一个临时的缓冲区,而后将数据copy到直接内存,而后再将这块直接内存上的数据发送到IO设备的缓冲区,最后销毁临时直接内存区域。而基于HeapBuffer的IO读流程也相似。使用DirectBuffer以后,避免了JVM堆内存和直接内存之间数据来回复制,在一些应用场景中性能有显著的提升。除了避免屡次拷贝以外直接内存的另外一个好处就是访问速度快,这跟JVM的对象访问方式有关。并发

DirectBuffer的缺点在于直接内存的分配与回收代价相对比较大,所以DirectBuffer适用于缓冲区能够重复使用的场景。工具


Netty中的Buffersoop

在Netty中,缓冲区有两种形式即HeapBuffer和DirectBuffer。Netty对于他们都进行了池化:性能

其中对应堆内存和直接内存的池化实现分别是PooledHeapByteBuf和PooledDirectByteBuf,在各自的实现中都维护着一个Recycler,这个Recycler就是本文关注的重点,也是Netty轻量级内存池技术的核心实现。学习


Recycler及内部组件测试

Recycler是一个抽象类,向外部提供了两个公共方法get和recycle分别用于从对象池中获取对象和回收对象;另外还提供了一个protected的抽象方法newObject,newObject用于在内存池中没有可用对象的时候建立新的对象,由用户本身实现,Recycler以泛型参数的形式让用户传入具体要池化的对象类型。this

/**
 * Light-weight object pool based on a thread-local stack.
 *
 * @param <T> the type of the pooled object
 */
public abstract class Recycler<T>

Recycler内部主要包含三个核心组件,各个组件负责对象池实现的具体部分,Recycler向外部提供统一的对象建立和回收接口:

  1. Handle

  2. WeakOrderQueue

  3. Stack

各组件的功能以下

Handle

Recycler在内部类中给出了Handle的一个默认实现:DefaultHandle,Handle主要提供一个recycle接口,用于提供对象回收的具体实现,每一个Handle关联一个value字段,用于存放具体的池化对象,记住,在对象池中,全部的池化对象都被这个Handle包装,Handle是对象池管理的基本单位。另外Handle指向这对应的Stack,对象存储也就是Handle存储的具体地方由Stack维护和管理。

Stack

Stack具体维护着对象池数据,向Recycler提供push和pop两个主要访问接口,pop用于从内部弹出一个可被重复使用的对象,push用于回收之后能够重复使用的对象。

WeakOrderQueue

WeakOrderQueue的功能能够由两个接口体现,add和transfer。add用于将handler(对象池管理的基本单位)放入队列,transfer用于向stack输入能够被重复使用的对象。咱们能够把WeakOrderQueue看作一个对象仓库,stack内只维护一个Handle数组用于直接向Recycler提供服务,当从这个数组中拿不到对象的时候则会寻找对应WeakOrderQueue并调用其transfer方法向stack供给对象。


Recycler实现原理

我先给出一张总的示意图,下面若是有看不懂的地方能够结合这张图来理解:

上图表明着Recycler的工做示意图。Recycler#get是向外部提供的从对象池获取对象的接口:

public final T get() {
    Stack<T> stack = threadLocal.get();
    DefaultHandle handle = stack.pop();
    if (handle == null) {
        handle = stack.newHandle();
        handle.value = newObject(handle);
    }
    return (T) handle.value;
}

Recycler首先从当前线程绑定的值中获取stack,咱们能够得知Netty中实际上是每一个线程关联着一个对象池,直接关联对象为Stack,先看看池中是否有可用对象,若是有则直接返回,若是没有则新建立一个Handle,而且调用newObject来新建立一个对象而且放入Handler的value中,newObject由用户本身实现。

当Recycler使用Stack的pop接口的时候,咱们看看:

DefaultHandle pop() {
    int size = this.size;
    if (size == 0) {
        if (!scavenge()) {
            return null;
        }
        size = this.size;
    }
    size --;
    DefaultHandle ret = elements[size];
    if (ret.lastRecycledId != ret.recycleId) {
        throw new IllegalStateException("recycled multiple times");
    }
    ret.recycleId = 0;
    ret.lastRecycledId = 0;
    this.size = size;
    return ret;
}

首先看看Stack的elements数组是否有对象可用,若是有则将size大小减1,返回对象。若是elements数组中已经没有对象可用,则须要从仓库中查找是够有能够用的对象,也就是scavenge的实现,scavenge具体调用的是scavengeSome。Stack的仓库是由WeakOrderQueue链接起来的链表实现的,Stack维护着链表的头部指针。而每一个WeakOrderQueue又维护着一个链表,节点由Link实现,Link的实现很简单,主要是继承AtomicInteger类另外还有一个Handle数组、一个读指针和一个指向下一个节点的指针,Link巧妙的利用AtomicInteger值来充当数组的写指针从而避免并发问题。

Recycler对象池的对象存储分为两个部分,Stack的Handle数组和Stack指向的WeakOrderQueue链表。

private DefaultHandle[] elements;
private volatile WeakOrderQueue head;
private WeakOrderQueue cursor, prev;

Stack保留着WeakOrderQueue链表的头指针和读游标。WeakOrderQueue链表的每一个节点都是一个Link,而每一个Link都维护者一个Handle数组。

池中对象的读取和写入

从对象池获取对象主要是从Stack的Handle数组,而Handle数组的后备资源来源于WeakOrderQueue链表。而elements数组和WeakOrderQueue链表中对象的来源有些区别:

public void recycle() {
    Thread thread = Thread.currentThread();
    if (thread == stack.thread) {
        stack.push(this);
        return;
    }
    // we don't want to have a ref to the queue as the value in our weak map
    // so we null it out; to ensure there are no races with restoring it later
    // we impose a memory ordering here (no-op on x86)
    Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
    WeakOrderQueue queue = delayedRecycled.get(stack);
    if (queue == null) {
        delayedRecycled.put(stack, queue = new WeakOrderQueue(stack, thread));
    }
    queue.add(this);
}

从Handle的recycle实现看出:若是由拥有Stack的线程回收对象,则直接调用Stack的push方法将该对象直接放入Stack的数组中;若是由其余线程回收,则对象被放入线程关联的<Stack,WeakOrderQueue>的队列中,这个队列其实在这里被放入了stack关联的WeakOrderQueue链表的表头:

WeakOrderQueue(Stack<?> stack, Thread thread) {
    head = tail = new Link();
    owner = new WeakReference<Thread>(thread);
    synchronized (stack) {
        next = stack.head;
        stack.head = this;
    }
}

每个没有拥有stack的线程回收对象的时候都会从新建立一个WeakOrderQueue节点放入stask关联的WeakOrderQueue链表的表头,这样作最终实现了多线程回收对象通通放入stack关联的WeakOrderQueue链表中而拥有stack的线程都可以读取其余线程供给的对象。


简单的测试数听说话

下面咱们来看下基于轻量级内存池和原始使用方式带来的性能数据对比,这里拿Netty提供的一个简单的能够回收的RecyclableArrayList来和传统的ArrayList来作比较,因为RecyclableArrayList和传统的ArrayList优点主要在于当频繁重复建立ArrayList对象的时候RecyclableArrayList不会真的新建立,而是会从池中获取对象来使用,而ArrayList的每次new操做都会在JVM的对内存中真枪实弹的建立一个对象,所以咱们能够想象对于ArrayList的使用,青年代的内存回收相对会比较频繁,为了简单期间,咱们这个例子不涉及直接内存技术,所以咱们关心的地方主要是GC频率回收的改善,看看个人两段测试代码:

代码1:

public static void main(String ...s) {
    int i=0, times = 1000000;
    byte[] data = new byte[1024];
    while (i++ < times) {
        RecyclableArrayList list = RecyclableArrayList.newInstance();
        int count = 100;
        for (int j=0;j<count;j++){
            list.add(data);
        }
        list.recycle();
        System.out.println("count:[" + count +
                "]");
        sleep(1);
    }
}

代码2:

public static void main(String ...s) {
    int i=0, times = 1000000;
    byte[] data = new byte[1024];
    while (i++ < times) {
        ArrayList list = new ArrayList();
        int count = 100;
        for (int j=0;j<count;j++){
            list.add(data);
        }
        System.out.println("count:[" + count +
                "]");
        sleep(1);
    }
}

上面代码逻辑相同,分别循环100w次,每次循环建立一个ArrayList对象,放入100个指向1kb大小的字节数组的引用,消耗内存的地方主要是ArrayList对象的建立,由于ArrayList的内部是对象数组实现的,所以内存消耗比较少,咱们只能经过快速的循环建立来达到内存渐变的效果。

上面左图是使用传统的ArrayList测试数据,右图是使用RecyclableArrayList的测试数据,对于不可循环使用的ArrayList,GC频率相比使用RecyclableArrayList的GC频率高不少,上面的工具也给出了左图16次GC花费的时间为77.624ms而右图的3次GC花费的时间为26.740ms。


Recycler对象池总结

在Netty中,全部的IO操做基本上都要涉及缓冲区的使用,不管是上文说的HeapBuffer仍是DirectBuffer,若是对于这些缓冲区不可以重复利用,后果是可想而知的。对于堆内存则会引起相对频繁的GC,而对于直接内存则会引起频繁的缓冲区建立与回收,这些操做对于两种缓冲区分别带来严重的性能损耗,Netty基于ThreadLocal实现的轻量级对象池实如今必定程度上减小了因为GC和分配回收带来的性能损耗,使得Netty线程运行的更快,整体性能更优。

整体上基于内存池技术的缓冲区实现,优势能够总结以下:

  1. 对于PooledHeapBuffer的使用,Netty能够重复使用堆内存区域,下降的内存申请的频率同时也下降了JVM GC的频率。

  2. 对于PooledDirectBuffer而言,Netty能够重复使用直接内存区域分配的缓冲区,这使得对于直接内存的使用在原有相比HeapBuffer的优势以外又弥补了自身分配与回收代价相对比较大的缺点。


这篇文章仅由做者根据对Netty源码的学习与理解所总结,如出现差错,还请多多指教。同时感谢以前有的同窗对我前面Netty相关文章中的错误指出,很是感谢!

相关文章
相关标签/搜索