Netty對象池實現(xiàn)分析

什么是對象池技術(shù)?對象池應用在哪些地方识啦?

對象池其實就是緩存一些對象從而避免大量創(chuàng)建同一個類型的對象永脓,類似線程池的概念。對象池緩存了一些已經(jīng)創(chuàng)建好的對象贸宏,避免需要時才創(chuàng)建對象造寝,同時限制了實例的個數(shù)。池化技術(shù)最終要的就是重復的使用池內(nèi)已經(jīng)創(chuàng)建的對象吭练。從上面的內(nèi)容就可以看出對象池適用于以下幾個場景:

  1. 創(chuàng)建對象的開銷大
  2. 會創(chuàng)建大量的實例
  3. 限制一些資源的使用

如果創(chuàng)建一個對象的開銷特別大诫龙,那么提前創(chuàng)建一些可以使用的并且緩存起來(池化技術(shù)就是重復使用對象,提前創(chuàng)建并緩存起來重復使用就是池化)可以降低創(chuàng)建對象時的開銷鲫咽。

會大量創(chuàng)建實例的場景赐稽,重復的使用對象可減少創(chuàng)建的對象數(shù)量叫榕,降低GC的壓力(如果這些對象的生命周期都很短暫,那么可以降低YoungGC的頻率姊舵;如果生命周期很長晰绎,那么可以避免掉這些對象被FullGC——生命周期長凝果,且大量創(chuàng)建俐巴,這里就要結(jié)合系統(tǒng)的TPS等考慮池的大小了)袍嬉。

對于限制資源的使用更多的是一種保護策略敢课,比如數(shù)據(jù)庫鏈接池苇瓣。除去這些對象本身的開銷外负蠕,他們對外部系統(tǒng)也會造成壓力串绩,比如大量創(chuàng)建鏈接對DB也是有壓力的聚凹。那么池化除了優(yōu)化資源以外构资,本身限制了資源數(shù)抽诉,對外部系統(tǒng)也起到了一層保護作用。

如何實現(xiàn)對象池吐绵?

開源實現(xiàn):Apache Commons Pool
自己實現(xiàn):Netty輕量級對象池實現(xiàn)

Apache Commons Pool開源軟件庫提供了一個對象池API和一系列對象池的實現(xiàn)迹淌,支持各種配置,比如活躍對象數(shù)或者閑置對象個數(shù)等己单。DBCP數(shù)據(jù)庫連接池基于Apache Commons Pool實現(xiàn)唉窃。

Netty自己實現(xiàn)了一套輕量級的對象池。在Netty中纹笼,通常會有多個IO線程獨立工作纹份,基于NioEventLoop的實現(xiàn),每個IO線程輪詢單獨的Selector實例來檢索IO事件廷痘,并在IO來臨時開始處理蔓涧。最常見的IO操作就是讀寫,具體到NIO就是從內(nèi)核緩沖區(qū)拷貝數(shù)據(jù)到用戶緩沖區(qū)或者從用戶緩沖區(qū)拷貝數(shù)據(jù)到內(nèi)核緩沖區(qū)笋额。這里會涉及到大量的創(chuàng)建和回收Buffer元暴,Netty對Buffer進行了池化從而降低系統(tǒng)開銷。

<h3>Netty對象池實現(xiàn)分析</h3>

上面提到了IO操作中會涉及到大量的緩沖區(qū)操作鳞陨,NIO提供了兩種Buffer最為緩沖區(qū):DirectByteBuffer和HeapByteBuffer昨寞。Netty在兩種緩沖區(qū)的基礎上進行了池化進而提升性能。

DirectByteBuffer
DirectByteBuffer顧名思義是直接內(nèi)存(Direct Memory)上的Byte緩存區(qū)厦滤,直接內(nèi)存不是JVM Runtime數(shù)據(jù)區(qū)域的一部分援岩,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域。簡單的說這部分就是機器內(nèi)存掏导,分配的大小等都和虛擬機限制無關(guān)享怀。JDK1.4中開始我們可以使用native方法在直接內(nèi)存上來分配內(nèi)存,并在JVM堆內(nèi)存上維持一個引用來進行訪問趟咆,當JVM堆內(nèi)存上的引用被回收后添瓷,這塊內(nèi)存被操作系統(tǒng)回收梅屉。

HeapByteBuffer
HeapByteBuffer是在JVM堆內(nèi)存上分配的Byte緩沖區(qū),可以簡單的理解為byte[]數(shù)組的一種封裝鳞贷∨魈溃基于HeapByteBuffer的寫流程通常要先在直接內(nèi)存上分配一個臨時的緩沖區(qū),將數(shù)據(jù)從Heap拷貝到直接內(nèi)存搀愧,然后再將直接內(nèi)存的數(shù)據(jù)發(fā)送到IO設備的緩沖區(qū)惰聂,之后回收直接內(nèi)存。讀流程也類似咱筛。使用DirectByteBuffer避免了不必要的拷貝工作搓幌,所以在性能上會有提升。

DirectByteBuffer的缺點在于分配和回收的的代價相對較大迅箩,因此DirectByteBuffer適用于緩沖區(qū)可以重復使用的場景溉愁。

Netty的池化實現(xiàn)

以Buffer為例,對應直接內(nèi)存和堆內(nèi)存饲趋,Netty的池化分別為PooledDirectByteBuffer和PolledHeapByteBuffer拐揭。

ByteBuffer繼承關(guān)系

通過PooledDirectByteBuffer的API定義可以看到,它的構(gòu)造方法是私有的篙贸,而創(chuàng)建一個實例的入口是:

    static PooledDirectByteBuf newInstance(int maxCapacity) {
        PooledDirectByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }

可見RECYCLER是池化的核心投队,創(chuàng)建對象時都通過RECYCLER.get來獲得一個實例(Recycler就是Netty實輕量級池化技術(shù)的核心)枫疆。

Recycler實現(xiàn)分析(源碼分析)

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

從注釋可以看出Netty基于thread-local實現(xiàn)了輕量級的對象池爵川。

Recycler成員

Recycler的API非常簡單:

  • get():獲取一個實例
  • recycle(T, Handle<T>):回收一個實例
  • newObject(Handle<T>):創(chuàng)建一個實例

get流程

    @SuppressWarnings("unchecked")
    public final T get() {
        if (maxCapacity == 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;
    }

get的簡化流程(這里先不深究細節(jié)):

  1. 拿到當前線程對應的stack
  2. 從stack中pop出一個元素
  3. 如果不為空則返回,否則創(chuàng)建一個新的實例

可以大概明白Stack是對象池化背后存儲實例的數(shù)據(jù)結(jié)構(gòu):如果能從stack中拿到可用的實例就不再創(chuàng)建新的實例息楔。

recycle流程

一個“池子”最核心的就是做兩件事情寝贡,第一個是上面的Get,即從池子中拿出一個可用的實例值依。另一個就是在用完后將數(shù)據(jù)放回到池子中(線程池圃泡、連接池都是這樣)。

    public final boolean recycle(T o, Handle<T> handle) {
        if (handle == NOOP_HANDLE) {
            return false;
        }

        DefaultHandle<T> h = (DefaultHandle<T>) handle;
        if (h.stack.parent != this) {
            return false;
        }

        h.recycle(o);
        return true;
    }


    public void recycle(Object object) {
        if (object != value) {
            throw new IllegalArgumentException("object does not belong to handle");
        }
        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);
    }

回收一個實例核心的步驟由以上兩個方法組成:Recycler的recycle方法和DefaultHandle的recycle方法愿险。
Recycler的recycle方法主要做了一些參數(shù)驗證颇蜡。
DefaultHandle的recycle方法流程如下:

  1. 如果當前線程是當前stack對象的線程,那么將實例放入stack中辆亏,否則:
  2. 獲取當前線程對應的Map<Stack, WeakOrderQueue>风秤,并將實例加入到Stack對應的Queue中。

從獲取實例和回收實例的代碼可以看出扮叨,整個對象池的核心實現(xiàn)由ThreadLocal和Stack及WrakOrderQueue構(gòu)成缤弦,接著來看Stack和WrakOrderQueue的具體實現(xiàn),最后概括整體實現(xiàn)彻磁。

Stack實體

Stack<T>
    parent:Recycler               // 關(guān)聯(lián)對應的Recycler
    thread:Thread                 // 對應的Thread
    elements:DefaultHandle<?>[]   // 存儲DefaultHandle的數(shù)組
    head:WeakOrderQueue           // 指向WeakOrderQueue元素組成的鏈表的頭部“指針”
    cursor,prev:WrakOrderQueue    // 當前游標和前一元素的“指針”

pop實現(xiàn)

    DefaultHandle<T> 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;
    }

  1. 如果size為0(這里的size表示stack中可用的元素)碍沐,嘗試進行scavenge狸捅。
  2. 返回elements中的最后一個元素。
    boolean scavenge() {
        // continue an existing scavenge, if any
        if (scavengeSome()) {
            return true;
        }

        // reset our scavenge cursor
        prev = null;
        cursor = head;
        return false;
    }

    boolean scavengeSome() {
        WeakOrderQueue cursor = this.cursor;
        if (cursor == null) {
            cursor = head;
            if (cursor == null) {
                return false;
            }
        }

        boolean success = false;
        WeakOrderQueue prev = this.prev;
        do {
            if (cursor.transfer(this)) {
                success = true;
                break;
            }

            WeakOrderQueue next = cursor.next;
            if (cursor.owner.get() == null) {
                // If the thread associated with the queue is gone, unlink it, after
                // performing a volatile read to confirm there is no data left to collect.
                // We never unlink the first queue, as we don't want to synchronize on updating the head.
                if (cursor.hasFinalData()) {
                    for (;;) {
                        if (cursor.transfer(this)) {
                            success = true;
                        } else {
                            break;
                        }
                    }
                }
                if (prev != null) {
                    prev.next = next;
                }
            } else {
                prev = cursor;
            }

            cursor = next;

        } while (cursor != null && !success);

        this.prev = prev;
        this.cursor = cursor;
        return success;
    }

簡要概括上面的流程就是Stack從“背后”的Queue中獲取可用的實例累提,如果Queue中沒有可用實例就遍歷到下一個Queue(Queue組成了一個鏈表)尘喝。

push實現(xiàn)

    void push(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) {
            // Hit the maximum capacity - drop the possibly youngest object.
            return;
        }
        if (size == elements.length) {
            elements = Arrays.copyOf(elements, Math.min(size << 1, maxCapacity));
        }

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

push相對pop流程要更加簡單,直接將回收的元素放到隊尾(實際是一個數(shù)組)斋陪。

WeakOrderQueue實體

WeakOrderQueue
    head,tail:Link          // 內(nèi)部元素的指針(WeakOrderQueue內(nèi)部存儲的是一個Link的鏈表)
    next:WeakOrderQueue     // 指向下一個WeakOrderQueue的指針
    owner:Thread            // 對應的線程

WeakOrderQueue核心包含兩個方法瞧省,add方法將元素添加到自身的“隊列”中,transfer方法將自己擁有的元素“傳輸”到Stack中鳍贾。

Linke結(jié)構(gòu)如下

    private static final class Link extends AtomicInteger {
        private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];

        private int readIndex;
        private Link next;
    }

Link內(nèi)部包含了一個數(shù)組用于存放實例鞍匾,同時標記了讀取位置的索引和下一個Link元素的指針。
結(jié)合Link的結(jié)構(gòu)骑科,Weak的結(jié)構(gòu)如下:


Link結(jié)構(gòu)

add方法

    void add(DefaultHandle<?> handle) {
        handle.lastRecycledId = id;

        Link tail = this.tail;
        int writeIndex;
        if ((writeIndex = tail.get()) == LINK_CAPACITY) {
            this.tail = tail = tail.next = new Link();
            writeIndex = tail.get();
        }
        tail.elements[writeIndex] = handle;
        handle.stack = null;
        // we lazy set to ensure that setting stack to null appears before we unnull it in the owning thread;
        // this also means we guarantee visibility of an element in the queue if we see the index updated
        tail.lazySet(writeIndex + 1);
    }

add操作將元素添加到tail指向的Link對象中橡淑,如果Link已滿則創(chuàng)建一個新的Link實例。

transfer方法

boolean transfer(Stack<?> dst) {

    Link head = this.head;
    if (head == null) {
        return false;
    }

    if (head.readIndex == LINK_CAPACITY) {
        if (head.next == null) {
            return false;
        }
        this.head = head = head.next;
    }

    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 = Math.min(srcStart + actualCapacity - dstSize, srcEnd);
    }

    if (srcStart != srcEnd) {
        final DefaultHandle[] srcElems = head.elements;
        final DefaultHandle[] dstElems = dst.elements;
        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");
            }
            element.stack = dst;
            dstElems[newDstSize ++] = element;
            srcElems[i] = null;
        }
        dst.size = newDstSize;

        if (srcEnd == LINK_CAPACITY && head.next != null) {
            this.head = head.next;
        }

        head.readIndex = srcEnd;
        return true;
    } else {
        // The destination stack is full already.
        return false;
    }
}

transfer方法收件根據(jù)stack的容量和自身擁有的實例數(shù)咆爽,計算出最終需要轉(zhuǎn)移的實例數(shù)梁棠。之后就是數(shù)組的拷貝和指標的調(diào)整。
基本上所有的流程有個大致的了解斗埂,下面從整體的角度回顧一下Netty對象池的實現(xiàn)符糊。

整體實現(xiàn)
結(jié)構(gòu)

整體結(jié)構(gòu)

整個設計上核心的幾點:
1. Stack相當于是一級緩存,同一個線程內(nèi)的使用和回收都將使用一個Stack
2. 每個線程都會有一個自己對應的Stack呛凶,如果回收的線程不是Stack的線程男娄,將元素放入到Queue中
3. 所有的Queue組合成一個鏈表,Stack可以從這些鏈表中回收元素(實現(xiàn)了多線程之間共享回收的實例)

整體結(jié)構(gòu)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末漾稀,一起剝皮案震驚了整個濱河市模闲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌崭捍,老刑警劉巖尸折,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異殷蛇,居然都是意外死亡实夹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門粒梦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亮航,“玉大人,你說我怎么就攤上這事谍倦∪福” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵昼蛀,是天一觀的道長宴猾。 經(jīng)常有香客問我圆存,道長,這世上最難降的妖魔是什么仇哆? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任沦辙,我火速辦了婚禮,結(jié)果婚禮上讹剔,老公的妹妹穿的比我還像新娘油讯。我一直安慰自己,他們只是感情好延欠,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布陌兑。 她就那樣靜靜地躺著,像睡著了一般由捎。 火紅的嫁衣襯著肌膚如雪兔综。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天狞玛,我揣著相機與錄音软驰,去河邊找鬼。 笑死心肪,一個胖子當著我的面吹牛锭亏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播硬鞍,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼慧瘤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了膳凝?” 一聲冷哼從身側(cè)響起碑隆,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤恭陡,失蹤者是張志新(化名)和其女友劉穎蹬音,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體休玩,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡著淆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了拴疤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片永部。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖呐矾,靈堂內(nèi)的尸體忽然破棺而出苔埋,到底是詐尸還是另有隱情,我是刑警寧澤蜒犯,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布组橄,位于F島的核電站荞膘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏玉工。R本人自食惡果不足惜羽资,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望遵班。 院中可真熱鬧屠升,春花似錦、人聲如沸狭郑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翰萨。三九已至微服,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缨历,已是汗流浹背以蕴。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辛孵,地道東北人丛肮。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像魄缚,于是被迫代替她去往敵國和親宝与。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 從三月份找實習到現(xiàn)在冶匹,面了一些公司习劫,掛了不少,但最終還是拿到小米嚼隘、百度诽里、阿里、京東飞蛹、新浪谤狡、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,253評論 11 349
  • 1卧檐、Netty基礎入門 Netty是由JBOSS提供的一個java開源框架墓懂。Netty提供異步的、事件驅(qū)動的網(wǎng)絡應...
    我是嘻哈大哥閱讀 4,690評論 0 31
  • Java SE 基礎: 封裝霉囚、繼承捕仔、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務)結(jié)合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,110評論 0 8
  • 一 我終于下決心要去那個城市了。 那個城市在哪其實我并不知道榜跌,只是我覺得非去不可了闸天。一無所知,毫無記憶斜做,卻在某一天...
    西江無鋒閱讀 355評論 5 2