Netty核心概念(10)以內存管理

1.前言

 以前的章節已經將啓動demo中能看見的內容都分析完了,Netty的一個總體樣貌都在第8節線程模型最後給的圖畫出來了。這些內容解釋了Netty爲何是一個異步事件驅動的程序,也解釋了Netty的線程模型的高效,可是並無涉及到的一個方面就是Handler的解析過程。經過前面的知識點咱們都應該明白了Handler用於對獲取的數據按照相關協議進行解析,Java的NIO都是經過buffer完成的讀寫的,這裏關於Netty的另外一個高效性卻沒有涉及,那就是內存管理,這個階段發生在handler讀取數據的階段。html

 Netty使用了什麼方式管理內存的?爲何須要內存管理?第一個問題是本節的主要內容,第二個問題在此給出答案。緣由在於IO操做涉及到頻繁的內存分配銷燬,若是是在堆上分配內存空間,將會使得GC操做很是頻繁,這對性能有極大的損耗,因此Netty使用了JDK1.5提供的堆外內存分配,程序能夠直接操做不歸JVM管轄的內存空間,可是須要本身控制內存的建立和銷燬。經過堆外內存的方式,避免了頻繁的GC,可是帶來了另一個問題堆外內存建立的效率十分的低,因此頻繁建立堆外內存更加糟糕。基於上述緣由,Netty最終設計了一個堆外內存池,申請了一大塊內存空間,而後對這塊內存空間提供管理接口,讓應用層不須要關注內存操做,可以直接拿到相關數據。可是Netty並無徹底放棄在堆上開闢內存,提供了相應的接口。java

 要知道Netty是如何管理內存的,還須要搞明白幾件事情。算法

 final ByteBufAllocator allocator = config.getAllocator();
 final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
 byteBuf = allocHandle.allocate(allocator);
 allocHandle.lastBytesRead(doReadBytes(byteBuf));

 這個是NioByteUnsafe的read方法中的一段代碼,能夠看出handler的一個read基本思路:獲取ByteBufAllocator,再獲取RecvByteBufAllocator.Handle,經過這兩個類獲取一個ByteBuf,最後將數據寫入這個ByteBuf中。這個步驟就說明了一些讀取數據的相關概念。再瞭解一下Nio的Allocator是DefaultChannelConfig的allocator字段,最終生成在ByteBufUtil的static方法中。其根據io.netty.allocator.type參數來決定使用哪一種類型的,默認有UnpooledByteBufAllocator和PooledByteBufAllocator兩種,即便用內存池或是不使用,Android平臺默認unpooled,其它平臺默認pooled。Nio默認的RecvByteBufAllocator則是AdaptiveRecvByteBufAllocator,這個設置在DefaultChannelConfig的構造方法裏。瞭解了這些咱們接下來分別看這三個類的體系結構。數組

2 ByteBufAllocator

 該接口實例必須在線程安全的環境下使用。該接口定義的方法分爲下面幾種:緩存

    buffer():分配一個ByteBuf,是direct(直接內存緩存)仍是heap(堆內存緩存)由具體實現決定。安全

    ioBuffer():分配一個ByteBuf,最好是適合IO操做的direct內存緩存。異步

    heapBuffer():分配一個堆內存緩存oop

    directBuffer():分配一個direct直接內存緩存性能

    compositeBuffer():分配一個composite緩存,是direct仍是heap由具體實現決定優化

    compositeHeapBuffer():分配一個composite的堆緩存

    compositeDirectBuffer():分配一個composite的直接緩存

    isDirectBufferPooled():是不是direct緩存池

    calculateNewCapacity():計算ByteBuffer須要擴展時使用的容量

 根據接口方法咱們能夠很清楚ByteBuf分爲direct和heap兩種類型,又由於分爲pool和unpool,因此笛卡兒積就是4中類型了。composite類型的定義將在ByteBuf時講解。

 2.1 AbstractByteBufAllocator

 抽象父類,定義了一些默認數據:1.默認緩存初始大小256,最大Integer.MAX_VALUE,components爲16,threshold是1048576 * 4(4MB)。

 抽象父類提供了包裝ByteBuf類成能夠檢測內存泄漏的靜態方法toLeakAwareBuffer。其還經過構造方法的參數preferDirect決定上面接口定義有歧義的方法是使用direct仍是buffer,爲true再知足平臺條件就使用direct,不然就是heap。ioBuffer方法只須要知足平臺支持,就會優先使用direct,和方法描述一致,平臺不支持就會使用heap。compositeBuffer相關方法都是直接建立了CompositeByteBuf對象,經過toLeakAwareBuffer方法包裝返回。calculateNewCapacity的邏輯是若是minNewCapacity等於threshold,就直接返回。大於就返回增長threshold的數值。小於從64開始*2,直到超過minNewCapacity。

 最終抽象父類包裝完了基本的方法,只剩下newHeapBuffer和newDirectBuffer方法交給子類來實現了。

2.2 PooledByteBufAllocator

 該類構造參數preferDirect爲false,因此其更傾向於使用heap內存,固然具體看是使用的哪一個方法了。默認的pageSize是8192,maxOrder是11,chunkSize是8192<<11,tinyCacheSize是512,smallCacheSize是256,normalCacheSize是64,對這些參數有所疑惑是正常的,具體能夠先看下Netty的內存管理文章:這裏。簡單看下就能明白這些參數的含義了。

    protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
        PoolThreadCache cache = threadCache.get();
        PoolArena<byte[]> heapArena = cache.heapArena;

        final ByteBuf buf;
        if (heapArena != null) {
            buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            buf = PlatformDependent.hasUnsafe() ?
                    new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
        }

        return toLeakAwareBuffer(buf);
    }


    protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        PoolThreadCache cache = threadCache.get();
        PoolArena<ByteBuffer> directArena = cache.directArena;

        final ByteBuf buf;
        if (directArena != null) {
            buf = directArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            buf = PlatformDependent.hasUnsafe() ?
                    UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }

        return toLeakAwareBuffer(buf);
    }

 上面是實現抽象父類未實現的內容。能夠看到都是分爲幾步走:1.獲取threadCache,線程本地緩存。2.獲取對應的PoolArena,heap的是byte[]對象,direct的是ByteBuffer對象。3.存在就分配一個,不存在就建立一個。4.最後都經過toLeakAwareBuffer包裝成內存泄漏檢測的buffer。

 threadCache就表面這個緩存是綁定了線程的,因此以前接口上就說明了必須保證使用時的線程安全。

2.3 UnpooledByteBufAllocator

 未使用池技術的沒有過多可講解的內容,不是本章重點。

    protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
        return PlatformDependent.hasUnsafe() ?
                new InstrumentedUnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
                new InstrumentedUnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
    }

    protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        final ByteBuf buf;
        if (PlatformDependent.hasUnsafe()) {
            buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
        } else {
            buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }
        return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
    }

 都是直接返回的,不作特別的說明。

3 RecvByteBufAllocator

 該接口就一個方法,生成一個Handler。

 Handler已經被ExtendedHandler取代了,不過也是直接繼承了Handler。allocate提供了分配方法,handler就是來管理如何分配。

  allocate():分配一個ByteBuf,要足夠大獲取全部輸入數據,也要足夠小不要浪費太多空間。

  guess():猜想allocate應該要分配多大的空間

  reset():重置累積的計數,並建議在下一個讀loop要讀取多少字節或者消息。應該使用continueReading方法判斷讀操做是否結束

  incMessagesRead():增長本次讀loop的讀取message的計數

  lastBytesRead():設置最後一次讀操做獲取的字節

  attemptedBytesRead():設置讀操做須要嘗試讀取的字節數

  continueReading():決定當前讀loop是否繼續

  readComplete():讀操做已完成。

3.1 AdaptiveRecvByteBufAllocator

  該類也設置了一些默認參數,最小大小爲64,最大爲65536,初始化大小爲1024,索引增加步長爲4,減小爲1。

    static {
        List<Integer> sizeTable = new ArrayList<Integer>();
        for (int i = 16; i < 512; i += 16) {
            sizeTable.add(i);
        }

        for (int i = 512; i > 0; i <<= 1) {
            sizeTable.add(i);
        }

        SIZE_TABLE = new int[sizeTable.size()];
        for (int i = 0; i < SIZE_TABLE.length; i ++) {
            SIZE_TABLE[i] = sizeTable.get(i);
        }
    }

 靜態方法初始化了一個SIZE_TABLE字段,類型爲int[]。能夠從這段邏輯看出改字段存儲了:16,32,48,64...496,512,1024,2048....2^31。這些元素。這些字段有什麼含義呢?具體看使用了這個表的方法:

    private static int getSizeTableIndex(final int size) {
        for (int low = 0, high = SIZE_TABLE.length - 1;;) {
            if (high < low) {
                return low;
            }
            if (high == low) {
                return high;
            }

            int mid = low + high >>> 1;
            int a = SIZE_TABLE[mid];
            int b = SIZE_TABLE[mid + 1];
            if (size > b) {
                low = mid + 1;
            } else if (size < a) {
                high = mid - 1;
            } else if (size == a) {
                return mid;
            } else {
                return mid + 1;
            }
        }
    }

 這個方法名就說明了其做用是獲取表的下標索引,這大致上是個二分查找的思路。給了一個初始大小,若是該值比中間值右邊還大,繼續,low從新設置;比中間值小,也繼續,high從新設置。若是等於中間值返回下標,若是介於中間值和中間值大一個的位置,返回中間值大一個的內容。

 這麼理解很差理解,再看上面的接口定義的allocate的方法,目標是不要浪費太多空間,也要知足可以讀取全部的內容。有了這麼個前提這個操做就好理解了,這是開發人員根據實際設計的一組空間分配的大小設置,根據size,來獲取一個合適的分配大小。這個方法就是獲取合適大小的表大小數組的下標。因此介於中間值和大於中間值的位置,就返回了大於中間值的位置,保證這兩個內容,足夠,浪費最小。至於爲何前面都是16累計,到了512就變成了*2累計,這個估計和實際狀況相關而設計的。

3.2 HandleImpl

 這個就是AdaptiveRecvByteBufAllocator提供的一個Handler了,回到最初給的代碼,經過recvallocator的handler,處理allocator,最終獲取讀取的buffer。ExtendedHandle定義的11個方法,這裏只實現了4個,其它的由父類MaxMessageHandle實現了。先看MaxMessageHandler類的相關內容。

  

  maxMessagePerRead:一次讀取最多的數據量

  totalMessages:總共的消息量

  totalBytesRead:總共的字節量(前面也提到過,分爲message和byte)

  attemptedBytesRead:嘗試讀取的字節數

  lastBytesRead:最後讀取的字節數

 大部分方法都是直接返回相關的字段,其它內容不值一提,須要結合實際使用進行分析。allocate方法,實際上調用了alloc的ioBuffer方法,即儘量的使用direct模式。

 HandleImpl實際也沒有太多內容,最難理解的就是上面說的獲取大小適宜的緩衝區的計算須要大小的步驟了。

 小結一下:RecvByteBufAllocator的目的就一個,爲了開闢合適大小的緩衝區。

4. ByteBuf

 此概念是最重要又不重要的一個環節。ByteBuf抽象類實現了ReferenceCounted,這個是Netty設計的direct內存一個重點。上面談到過heap模式存在頻繁的GC,direct模式若是頻繁開闢緩存和銷燬,性能更低,因此採起了Pool的方式管理direct。而實際上使用池的技術也須要標記已使用的,和未使用的區域,使用完成以後也須要進行釋放。Netty採起了一種GC策略,引用計數法。有一個類引用了該Buffer,+1,release的時候-1。爲0的時候就都不使用了,這個時候該區域就能夠進行釋放。這個就是ReferenceCounted所作的事情,引用計數。

  refCnt:當前計數,爲0意味着收回

  retain:引用+1,或者+指定數值

  touch:調試使用,記錄當前訪問該對象的位置。ResourceLeakDetector能夠提供相關信息。

  release:引用-1,或者-指定數值

 抽象父類ByteBuf的方法不少,這裏不進行截圖,簡單介紹說明(太多了,多數看名稱就能明白相關意思):

  capacity():當前容量;傳入數值,小於當前容量截斷,大於擴容到這個值

  maxCapacity():容許的最大容量

  alloc():獲取開闢這個緩存的alloc對象

  order():設置解析字節碼的字節順序,換另外一種說法可能更清楚(Big-endian和Little-endian),具體見wiki:這裏。該方法被廢棄,後面直接使用getShort和getShortLE進行區分。

  unwarp():若是該類是包裝類,返回未包裝前的對象。

  isDirect():是不是直接內存

  isReadOnly():是否只讀

  asReadOnly():返回一個只讀的ByteBuf版本

  readerIndex():返回讀取下標,帶參數就是設置這個值

  writerIndex():返回寫下標,帶參數就是設置

 其它的一些方法有:判斷可讀可寫狀態,和相關字節數。清除內容,標記讀位置,重置讀位置,標記寫和重置寫。丟棄一些讀取字節。而後就是基本的讀取int,short等基本類型的方法,這裏以前也說過多了一個LE方法。跳過一些字節。拷貝緩存,返回可讀的字節片斷,複製字節,內存地址等一系列方法。

 ByteBuf的相關方法定義實際上和JAVA提供的ByteBuffer區別不是很大,保持了一致性。下面咱們只介紹兩種ByteBuf。

4.1 PooledUnsafeDirectByteBuf

 這種buf可能就是咱們想要研究的了,從最上層的抽象父類一層層往下看其相關操做。

  1.AbstractByteBuf:

  該抽象類解決了基本的方法:可讀可寫的相關判斷。清除,標記讀寫位置,丟棄字節。其餘的方法大致都交給子類實現了,父類只是肯定了一下安全性,好比getInt是否是有2個可讀字節之類的。

  2.AbstractReferenceCountedByteBuf:

  該抽象父類最大的做用就是完成了以前引用計數的問題,經過CAS操做更新。相關方法的實現。

  3.PooledByteBuf:這個方法就是相關內存池的操做。

  這個類就實現了一些基本的capacity、slice、duplicate、deallocate的相關方法,基本上是經過上圖的參數實現的。

 最後就是咱們要說的PooledUnsafeDirectByteBuf類了,該類都是經過UnsafeByteBufUtil類來實現相關方法。實際上看過去這些方法都不復雜,由於複雜的部分被簡單略過了,好比分配緩存的時候具體是如何操做的,那個是在Pool方法中,使用緩存的方法卻是不復雜,可是有兩個內容也忽略了,一個是leakDetector另外一個就是recycler了。這些都不進行介紹。

4.2 CompositeByteBuf

 這個類是以前沒有介紹清楚的,組合型ByteBuf,究竟是什麼含義呢?其實該類與Netty中的零拷貝有關。這個零拷貝和最初節所說的操做系統層次的零拷貝不是一個概念,Netty的零拷貝指的是用戶態之間的內存拷貝。即減小從用戶地址的一個位置拷貝到另外一個地址的次數。好比說文件FileRegion,能夠經過transferTo方法將文件緩衝直接交給Channel,而不用經過while循環獲取數據,再傳遞。又好比一個消息由多個模塊組成,好比請求頭,請求體。一般要合併成一個Buffer,這個就產生了在用戶態中進行拷貝數據了。這個解決方法也就是如今所說的CompositeByteBuf,組合ByteBuf。將組件添加到這個buffer中,再對操做層透明的傳輸這些數據。

 components是一個ArrayList,元素就是ByteBuf,maxNumComponents就是最大的組件個數,默認16個。

    private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) {
        assert buffer != null;
        boolean wasAdded = false;
        try {
            checkComponentIndex(cIndex);

            int readableBytes = buffer.readableBytes();

            // No need to consolidate - just add a component to the list.
            @SuppressWarnings("deprecation")
            Component c = new Component(buffer.order(ByteOrder.BIG_ENDIAN).slice());
            if (cIndex == components.size()) {
                wasAdded = components.add(c);
                if (cIndex == 0) {
                    c.endOffset = readableBytes;
                } else {
                    Component prev = components.get(cIndex - 1);
                    c.offset = prev.endOffset;
                    c.endOffset = c.offset + readableBytes;
                }
            } else {
                components.add(cIndex, c);
                wasAdded = true;
                if (readableBytes != 0) {
                    updateComponentOffsets(cIndex);
                }
            }
            if (increaseWriterIndex) {
                writerIndex(writerIndex() + buffer.readableBytes());
            }
            return cIndex;
        } finally {
            if (!wasAdded) {
                buffer.release();
            }
        }
    }    

    private Component findComponent(int offset) {
        checkIndex(offset);

        for (int low = 0, high = components.size(); low <= high;) {
            int mid = low + high >>> 1;
            Component c = components.get(mid);
            if (offset >= c.endOffset) {
                low = mid + 1;
            } else if (offset < c.offset) {
                high = mid - 1;
            } else {
                assert c.length != 0;
                return c;
            }
        }

        throw new Error("should not reach here");
    }

 能夠看見添加組件,和定位哪一個組件的方法,這樣就簡單的實現了將多個ByteBuf合併,對外提供透明接口,不用具體開闢新的空間,再拷貝相關數據了。

5 後記

 本節題目雖然叫作內存管理,可是核心內容較少,涉及算法,比較繁瑣。但大致介紹了一下Netty的內存設計思路,將了與之相關的類的實現。概括以下:

    1.Netty大體有4中內存方法,Heap Pool,Direct Pool, Heap unPool,Dircet unPool。

    2.Pool和UnPool的大致區別

    3.ByteBuf採起了引用計數的方式管理這塊被分配了的內存

    4.Netty零拷貝實現之一CompositeByteBuf,將多個ByteBuf組合起來,提供操做一個的接口。

 最重要的Pool方式的實現沒有說明,並且內存泄露檢測,recycle等內容也沒有說明。這些都須要本身去琢磨。再附上文中提到的一個其它博客對這塊算法的介紹:這裏,做爲這章內容的缺失補充。再補充一下Netty零拷貝的相關知識:這裏

 最後再給出一個別人使用Netty進行優化的例子:這裏,結合前面的知識,這個應該可以看懂。另外,此係列基本就到此爲止了,其它的應該不會再進行詳細介紹,後面可能會補充一些具體使用的demo。

相關文章
相關標籤/搜索