Netty源碼解析一 -- ByteBuf

JDK NIO之ByteBuffer的侷限性如下:
(1)長度固定,一旦分配完成,它的容量將不能動態擴展和收縮,而需要編碼的POJO對象大雨ByteBuffer的容量時,會發生索引越界異常;
(2)只有一個標識位置的指針position,讀寫的是偶需要搜公條用flip()和rewind()等,使用着必須小心的處理這些API,否則很容易導致程序越界異常;
(3)ByteBuffer的API功能有限,一些高級和實用扽特性不支持,需要使用者自己編程實現、
爲了彌補這些不足,Netty提供了自己的緩衝區實現ByteBuf。
ByteBuf與ByteBuf一樣維護了一個byte數組,提供以下幾類基本功能;
* 7中java基礎類型,byte數組,ByteBuffer等的讀寫;
* 緩衝區自身的copy和slice等;
* 設置網絡字節序;
* 構造緩衝區實例;
* 操作位置指針等方法;
ByteBuf通過兩個位置指針來協助緩衝的讀寫操作:讀指針:readerIndex和寫指針writerIndex;
因爲netty中ByteBuf的讀寫索引比較簡單,這裏對於讀寫索引的關係及相關的API不做詳細的介紹,感興趣的讀者可以去相關的API參考。
一、ByteBuf與ByteBuffer的相互轉換:
ByteBuf與ByteBuffer的相互轉換:
 @Override
    public ByteBuffer nioBuffer() {
        return nioBuffer(readerIndex, readableBytes());
    }
nioBuffer的具體實現這裏使用PooledHeapByteBuf中的實現來看:
    @Override
    public ByteBuffer nioBuffer(int index, int length) {
        checkIndex(index, length);
        index = idx(index);
        ByteBuffer buf =  ByteBuffer.wrap(memory, index, length);
        return buf.slice();
    }

一、ByteBuf的繼承結構:

ByteBuf可以分爲兩類:

(1)對內存:HeapByteBuf自己緩衝區,特點是內存的分配和回收速度快,可以被JVM自動回收,,缺點是如果使用Socket的IO讀寫,需要額外做一次內存複製,將堆內存對應的額緩衝區複製到內核Channel中,性能會有一定的下降。

(2)直接內存。DirectByteBuf字節緩衝區也可以叫做直接緩衝區,非堆內存。它在堆外進行內存分配,相比於堆內存,它的分配和回收速度會慢一些。但是將它寫入或者從SocketChannel中讀取時,由於少了一次內存複製。速度比堆內存要快。

因此Netty提供了多種ByteBuf 的實現共開發者選擇。在長期的開發實踐中,表明,在IO通信線程的讀寫緩衝區使用DirectByteBuf, 後端業務消息的編解碼模塊使用HeapByteBuf,這樣組合可以達到性能最優。

從內存回收的角度看,ByteBuf也分爲兩類:基於對象池的ByteBuf和普通ByteBuf。兩者的主要區別就是基於對象池的ByteBuf可以重用ByteBuf對象,它自己創建了一個內存池,可以循環利用創建的額ByteBuf,提升內存的使用效率,降低由於高負載導致的頻繁GC。測試表明使用內存池後的Netty在高負載,大併發衝擊下的內存和GC更加平穩。

二、AbstractByteBuf部分源碼介紹:

一、讀操作:
 @Override
    public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
        checkReadableBytes(length);
        getBytes(readerIndex, dst, dstIndex, length);
        readerIndex += length;
        return this;
    }

    @Override
    public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
        checkDstIndex(index, length, dstIndex, dst.length);
        System.arraycopy(memory, idx(index), dst, dstIndex, length);
        return this;
    }
二、寫操作:
    @Override
    public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length) {
        ensureAccessible();
        ensureWritable(length);
        setBytes(writerIndex, src, srcIndex, length);
        writerIndex += length;
        return this;
    }

    @Override
    public ByteBuf ensureWritable(int minWritableBytes) {
        if (minWritableBytes < 0) {
            throw new IllegalArgumentException(String.format(
                    "minWritableBytes: %d (expected: >= 0)", minWritableBytes));
        }

        if (minWritableBytes <= writableBytes()) {
            return this;
        }

        if (minWritableBytes > maxCapacity - writerIndex) {
            throw new IndexOutOfBoundsException(String.format(
                    "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                    writerIndex, minWritableBytes, maxCapacity, this));
        }

        // Normalize the current capacity to the power of 2.
        int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);

        // Adjust to the new capacity.
        capacity(newCapacity);
        return this;
    }

// 擴容機制:首先設置閾值爲4M.當需要的新容量正好等於閾值,則使用閾值作爲新的緩衝區容量。
// 如果新申請的內存空間大於閾值,不能採用倍增的方式(防止內存膨脹和浪費)擴張內存。
// 採用每次步進4M的方式進行內存擴張。擴張的時候需要對擴張後的那次u你和最大內存進行比較,
// 如果大於緩衝區的最大長度,則用maxCapacity作爲擴容的緩衝區容量。如果擴容後的新容量小於閾值,則以64爲基礎進行倍增。
// 直到倍增後的結果大於或等於需要的容量值。採用倍增或步進算法的原因是:如果以minNewCapacity作爲目標容量,
// 則本次擴容後的科協字節數剛好夠本次寫入使用。吸入完成後,他的可寫字節數會變成0,下次需要寫入的時候,
// 需要再次進行動態擴張,由於動態擴張需要進行內存複製。頻繁的內存複製會導致性能下降。
    @Override
    public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
        if (minNewCapacity < 0) {
            throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expectd: 0+)");
        }
        if (minNewCapacity > maxCapacity) {
            throw new IllegalArgumentException(String.format(
                    "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
                    minNewCapacity, maxCapacity));
        }
        final int threshold = 1048576 * 4; // 4 MiB page

        if (minNewCapacity == threshold) {
            return threshold;
        }

        // If over threshold, do not double but just increase by threshold.
        if (minNewCapacity > threshold) {
            int newCapacity = minNewCapacity / threshold * threshold;
            if (newCapacity > maxCapacity - threshold) {
                newCapacity = maxCapacity;
            } else {
                newCapacity += threshold;
            }
            return newCapacity;
        }

        // Not over threshold. Double up to 4 MiB, starting from 64.
        int newCapacity = 64;
        while (newCapacity < minNewCapacity) {
            newCapacity <<= 1;
        }

        return Math.min(newCapacity, maxCapacity);
    }

三、AbstractReferenceCountedByteBuf源碼分析:

從類的名字九可以看出該類主要是對引用進行技術,類似與JVM中的對象引用計數器,用於跟蹤對象的引用和銷燬,做自動內存回收。

CAS :compareAndSet,獲取當前變量的值,根據變量計算出一個新值,如果這時變量的值沒有變化,就用新值更新變量,如果有變化則不更新。
僞代碼:
for(;;;)
{
    獲取變量值A
    計算變量的新值保存在B
    如果變量A的值未變化,用B的值更新A,退出循環
    如果變量A的值有變化,繼續這個循環,直至更新成功才退出循環
}
1.成員變量:
// 通過原子的方式對成員變量進行更新等操作,以實現線程安全,消除鎖。
// public static AtomicIntegerFieldUpdaternewUpdater(Class tclass, 
// String fieldName) 這裏就不詳細分析他的源碼了,其實很簡單,他讓tclass的成員fieldName具有了原子性,是不是很簡單~
 private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater;

    static {
        AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> updater =
                PlatformDependent.newAtomicIntegerFieldUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
        if (updater == null) {
            updater = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
        }
        refCntUpdater = updater;
    }
    
    // 用於跟蹤對象的引用次數
    private volatile int refCnt = 1;
 @Override
    public ByteBuf retain() {
        for (;;) {
            int refCnt = this.refCnt;
            if (refCnt == 0) {
                throw new IllegalReferenceCountException(0, 1);
            }
            if (refCnt == Integer.MAX_VALUE) {
                throw new IllegalReferenceCountException(Integer.MAX_VALUE, 1);
            }
            if (refCntUpdater.compareAndSet(this, refCnt, refCnt + 1)) {
                break;
            }
        }
        return this;
    }
每調用一次retain方法,計數器加一 
compareAndSet方法用來獲取自己的值和期望的值進行比較,如果其間被其他線程修改了,那麼比對失敗,進行自旋操作,重新獲得計數器重新比較 
compareAndSet這個方法是CAS操作,由操作系統層面提供。
    @Override
    public final boolean release() {
        for (;;) {
            int refCnt = this.refCnt;
            if (refCnt == 0) {
                throw new IllegalReferenceCountException(0, -1);
            }

            if (refCntUpdater.compareAndSet(this, refCnt, refCnt - 1)) {
                if (refCnt == 1) {
                    deallocate();
                    return true;
                }
                return false;
            }
        }
    }
需要注意的是:黨refCnt == 1時,意味着申請和釋放相等,說明對象引用已經不可達, 該對象需要被釋放和垃圾回收掉, 則通過調用deallocate方法來釋放ByteBuf對象

四、PooledByteBuf內存池原理分析:

1.PoolArena:Arena本身是指一開區域,在內存管理中,MemoryArena是指內存中的一大塊連續的區域,PoolA人啊就是Netty的內存池實現類。爲了集中管理內存的分配和釋放,同時提高分配的釋放內存時候的性能,很多框架和應用都會預先申請一大塊內存,然後通過提供相應的分配和釋放接口來使用內存,這樣一來, 對內存的管理就被集中到幾個類或函數中,由於不在頻繁使用系統條用來申請和釋放內存,應用或者系統的性能也會大大提高,在這種涉及思路下,預先申請的一大塊內存就被成爲Memory Arena。

不同的框架,Memory Arena的實現不同,Netty的PoolArena是由多個Chunk組成的大塊內存區域,而每個Chunk則由一個或多個Page組成,因此,對內存的組織和管理也就主要集中在如何管理和組織Chunk和Page了, PoolArena中的內存Chunk定義如下所示。

abstract class PoolArena<T> {

    static final int numTinySubpagePools = 512 >>> 4;

    final PooledByteBufAllocator parent;

    private final int maxOrder;
    final int pageSize;
    final int pageShifts;
    final int chunkSize;
    final int subpageOverflowMask;
    final int numSmallSubpagePools;
    private final PoolSubpage<T>[] tinySubpagePools;
    private final PoolSubpage<T>[] smallSubpagePools;

    private final PoolChunkList<T> q050;
    private final PoolChunkList<T> q025;
    private final PoolChunkList<T> q000;
    private final PoolChunkList<T> qInit;
    private final PoolChunkList<T> q075;
    private final PoolChunkList<T> q100;
  • qInit:存儲剩餘內存0-25%的chunk
  • q000:存儲剩餘內存1-50%的chunk
  • q025:存儲剩餘內存25-75%的chunk
  • q050:存儲剩餘內存50-100%個chunk
  • q075:存儲剩餘內存75-100%個chunk
  • q100:存儲剩餘內存100%chunk

CHunk主要用來組織和管理多個Page的內存分配和釋放,在Netty中,Chunk中的Page被分配成一個二叉樹,假設一個Chunk由8個Page組成,那麼這些Page將會被按照如下圖方式組織:

                                                        Chunk
                                                            |
                                                            |
                       page                                                         page
                           |                                                                |
                           |                                                                |
         page                        page                           page                page
             |                                |                                |                        |
             |                                |                                |                        |

   page    page            page        page       page        page    page    page   

page的大小是8個字節,Chunk的大小是64個字節,真棵樹有4層,第一層(野子節點所在的層)用來分配page的內存,第三層用來分配兩個page的內存,一次類推。每個節點都記錄了自己咋整個Memory Arena中的偏移地址,黨一個節點代表的內存區域被分配出去之後,這個節點就會被標記爲已分配,自這個節點一下的所有節點在後面的內存分配請求都會被忽略,舉例來說,當我們請求一個16字節的內存時,上面這個樹中第二層的4個節點中的一個就會被標記爲已分配,這就表示整個MemoryArena中有16個字節被分配出去了,新的分配請求只能從剩下的三個節點及其子樹中去尋找合適的節點。對樹的遍歷採用深度優先的算法,但是在選擇哪個子節點繼續遍歷時則是隨即的,並不像通常的深度優先算法中那樣總是訪問左邊的子節點。

PoolSubpage

對於小於一個page的內存,Netty在Page中完成分配。每個page會被切分成大小相同的多個存儲塊。存儲快的大小都由第一次申請的內存塊大小決定,嘉定一個page是8個字節,如果第一次申請的是4個字節,則這個page就包含兩個數據塊,如果第一次申請的是8個自己,那麼這個page就包含一個數據塊。

一個page只能用戶分配與第一次申請時大小相同的內存,比如:一個4字節的page,如果第一次分配了1字節的內存,那麼後面這個page只能繼續分配1字節的內存,如果有一個申請了2字節內存的請求,就需要在一個新的page中中進行分配;

Page中存儲區域的使用狀態通過一個long數組進行維護,數組中每個long的每一位表示一個塊存儲區域的佔用情況:0表示未佔用,1表示已佔用。對於一餓4字節的page來說,如果這個page用來分配一個字節的存儲區域,那麼long’數組中就只有一個long類型的元素。這個熟知的低4位用來指示各個存儲區域的佔用情況。對於一個128字節的Page來說,如果這個Page也是用來分配1個字節的存儲區域。那麼long數組中就會包含2個元素,總共128位,每一位代表一個區域的佔用情況。

無論是Chunk還是Page,都是通過狀態位來標識內存是否可用。不同之處是CHunk通過在二叉樹上對節點進行標識實現。Page是通過維護塊的使用狀態標識來實現。                 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章