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相關文章中的錯誤指出,很是感謝!

相關文章
相關標籤/搜索