Netty9# Netty抽象內存分配器實現原理

前言

本文通過分析抽象內存分配器API梳理其基於堆內存、堆外內存分配的實現原理。最後走查了CompositeByteBuf這種類似數據庫視圖的實現原理。

一、內存分配器概覽

堆外內存&堆內存

分配方式 優點 缺點
堆內存 JVM負責內存的分配與回收 數據過多會引起頻繁GC和停頓;
多一次拷貝,在用戶態分配、I/O通信需要數據拷貝到內核態
堆外內存 I/O性能高,直接在內核態分配
降低GC頻率和停頓
內存分配和收回比較慢、需要手動處理

內存分配器類圖

字節緩存的分配出自ByteBufAllocator,其實現類AbstractByteBufAllocator(抽象類)、PooledByteBufAllocator(池化內存分配器)、UnpooledByteBufAllocator(非池化內存分配器)、PreferHeapByteBufAllocator(堆內存分配器)、PreferredDirectByteBufAllocator(堆外內存分配器)。

主要接口

接口 說明
ByteBuf buffer() 分配一塊字節緩存,由其實現類決定堆外內存或者堆內存
ByteBuf ioBuffer() 系統支持UNSAFE和CLEANER則優先分配堆外內存;否則分配堆內存。
ByteBuf heapBuffer() 分配堆內存字節緩存區
ByteBuf directBuffer() 分配堆外內存字節緩存區
CompositeByteBuf compositeBuffer() 分配一個CompositeByteBuf(將多個buffers組合成一個buffer)
由實現類決定堆內存或者堆外內存

二、內存分配器API解讀

下面走查下抽象內存分配器AbstractByteBufAllocator的API。

構造函數

private final boolean directByDefault;
private final ByteBuf emptyBuf;
protected AbstractByteBufAllocator(boolean preferDirect) {
  directByDefault = preferDirect && PlatformDependent.hasUnsafe(); // 註解@1
  emptyBuf = new EmptyByteBuf(this);
}

註解@1 directByDefault是否使用堆外內存分配,滿足兩個條件。preferDirect布爾型用戶傳入;PlatformDependent.hasUnsafe() 系統是否支持UNSAFE(通過內存指針進行堆外內存分配);即:用戶傳入preferDirect=true並且系統支持UNSAFE則使用堆外內存。

buffer()

@Override
public ByteBuf buffer() // 註解@2
  if (directByDefault) {
    return directBuffer();
  }
  return heapBuffer();
}

註解@2 directByDefault如果爲true使用堆外內存分配DirectByteBuffer,底層使用unsafe.allocateMemory分配。

directByDefault如果爲false使用堆內存分配 new byte[initialCapacity]。

ioBuffer

public ByteBuf ioBuffer() // 註解@3
  if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
  return directBuffer(DEFAULT_INITIAL_CAPACITY);
  }
  return heapBuffer(DEFAULT_INITIAL_CAPACITY);
}

註解@3 如果系統支持UNSAFE或者使用池化內存,優先分配堆外內存,否則分配堆內存。

heapBuffer

@Override
public ByteBuf heapBuffer() // 註解@4
 return heapBuffer(DEFAULT_INITIAL_CAPACITY, DEFAULT_MAX_CAPACITY);
}

註解@4 分配堆外內存new一個byte數組(new byte[])。

directBuffer

@Override
public ByteBuf directBuffer() // 註解@5
 return directBuffer(DEFAULT_INITIAL_CAPACITY, DEFAULT_MAX_CAPACITY);
}

註解@5 分配堆外內存。

compositeBuffer

@Override
public CompositeByteBuf compositeBuffer() // 註解@6
  if (directByDefault) {
   return compositeDirectBuffer();
  }
 return compositeHeapBuffer();
}

註解@6 跟上面一樣的,只是分配的CompositeByteBuf。下面看下這種將多個個buffer組合成一個buffer是如何實現的。

@Override
public CompositeByteBuf compositeHeapBuffer() {
 return compositeHeapBuffer(DEFAULT_MAX_COMPONENTS);
}
@Override
public CompositeByteBuf compositeHeapBuffer(int maxNumComponents) {
 return toLeakAwareBuffer(new CompositeByteBuf(thisfalse, maxNumComponents));
}
private CompositeByteBuf(ByteBufAllocator alloc, boolean direct, int maxNumComponents, int initSize) {
  super(AbstractByteBufAllocator.DEFAULT_MAX_CAPACITY);

  this.alloc = ObjectUtil.checkNotNull(alloc, "alloc");
  if (maxNumComponents < 1) {
  throw new IllegalArgumentException(
  "maxNumComponents: " + maxNumComponents + " (expected: >= 1)");
  }

  this.direct = direct;
  this.maxNumComponents = maxNumComponents;
  components = newCompArray(initSize, maxNumComponents); // 註解@6.1
}

註解@6.1 在CompositeByteBuf的構造方法中初始化了一個components,這個默認initSize=0;maxNumComponents默認爲16。

private static Component[] newCompArray(int initComponents, int maxNumComponents) {
 int capacityGuess = Math.min(AbstractByteBufAllocator.DEFAULT_MAX_COMPONENTS, maxNumComponents);
 return new Component[Math.max(initComponents, capacityGuess)]; // 註解@6.2
}

註解@6.2 components是Component的對象數組,數組大小默認爲16.


三、CompositeByteBuf實現原理

下面通過例子來體驗一把CompositeByteBuf,先直觀感受下。

示例代碼

@Test
public void testCompositeByteBuf(){
  String str1 = "瓜農";
  String str2 = "老梁";
  ByteBuf buf1 = Unpooled.buffer(10);
  buf1.writeBytes(str1.getBytes(CharsetUtil.UTF_8));
  System.out.println("buf1's readerIndex:" + buf1.readerIndex());
  System.out.println("buf1's writeIndex" + buf1.writerIndex());

  ByteBuf buf2 = Unpooled.buffer(10);
  buf2.writeBytes(str2.getBytes(CharsetUtil.UTF_8));
  System.out.println("buf2's readerIndex:" + buf2.readerIndex());
  System.out.println("buf2's writeIndex" + buf2.writerIndex());

  ByteBuf compositeByteBuf = Unpooled.wrappedBuffer(buf1,buf2);
  System.out.println("compositeByteBuf's readerIndex:" + compositeByteBuf.readerIndex());
  System.out.println("compositeByteBuf's writeIndex" + compositeByteBuf.writerIndex());
  System.out.print(compositeByteBuf.toString(CharsetUtil.UTF_8));
}

結果輸出

buf1's readerIndex:0
buf1'
s writeIndex6
buf2's readerIndex:0
buf2'
s writeIndex6
compositeByteBuf's readerIndex:0
compositeByteBuf'
s writeIndex12
瓜農老梁

小結  Unpooled.wrappedBuffer(buf1,buf2)將兩個ByteBuf進行了合併一個ByteBuf;對外提供統一的讀寫指針供使用。

接下來看下他是如何合併的。

public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuf... buffers) {
        switch (buffers.length) {
        case 0:
            break;
        case 1:
            ByteBuf buffer = buffers[0];
            if (buffer.isReadable()) {
                return wrappedBuffer(buffer.order(BIG_ENDIAN));
            } else {
                buffer.release();
            }
            break;
        default:
            for (int i = 0; i < buffers.length; i++) {
                ByteBuf buf = buffers[i];
                if (buf.isReadable()) {
                    return new CompositeByteBuf(ALLOC, false, maxNumComponents, buffers, i); // 註解@7
                }
                buf.release();
            }
            break;
        }
        return EMPTY_BUFFER;
    }

註解@7 通過創建一個CompositeByteBuf,將ByteBuf數組傳入構造函數。

CompositeByteBuf(ByteBufAllocator alloc, boolean direct, int maxNumComponents,
            ByteBuf[] buffers, int offset) {
            this(alloc, direct, maxNumComponents, buffers.length - offset);

 addComponents0(false0, buffers, offset); // 註解@8
 consolidateIfNeeded();
 setIndex0(0, capacity()); // 註解@9
}

註解@8  填充Component[]數據,每個Component元素包含了傳入的ByteBuf。

private CompositeByteBuf addComponents0(boolean increaseWriterIndex,
            final int cIndex, ByteBuf[] buffers, int arrOffset)
 
{
     //  buffers數組的長度;本例中arrOffset=0;count=len
        final int len = buffers.length, count = len - arrOffset;
        int ci = Integer.MAX_VALUE;
        try {
            checkComponentIndex(cIndex); // 合法性校驗
            shiftComps(cIndex, count); // 註解@8.1
            int nextOffset = cIndex > 0 ? components[cIndex - 1].endOffset : 0;
            for (ci = cIndex; arrOffset < len; arrOffset++, ci++) { // 註解@8.2
                // 從數組中拿出傳入的ByteBuf
               ByteBuf b = buffers[arrOffset];
                if (b == null) {
                    break;
                }
                // 構建Component
                Component c = newComponent(ensureAccessible(b), nextOffset); 
               // 加入components數組
                components[ci] = c;
                // 遞增endOffset
                nextOffset = c.endOffset;
            }
            return this;
        } finally {
            // ...
        }
}

註解@8.1 擴容Component數組,默認的數量爲16個,當添加的buffer的數量超過16時就需要擴容了,下面看下其如何擴容的。

private void shiftComps(int i, int count) {
        final int size = componentCount, newSize = size + count;
        assert i >= 0 && i <= size && count > 0;
        if (newSize > components.length) {
            // grow the array 
           int newArrSize = Math.max(size + (size >> 1), newSize); // 註解@8.1.1
            Component[] newArr;
            // 註解@8.1.2
            if (i == size) {
                newArr = Arrays.copyOf(components, newArrSize, Component[].class);
            } else {
                newArr = new Component[newArrSize];
                if (i > 0) {
                    System.arraycopy(components, 0, newArr, 0, i);
                }
                if (i < size) {
                    System.arraycopy(components, i, newArr, i + count, size - i);
                }
            }
            components = newArr;
        } else if (i < size) {
            System.arraycopy(components, i, components, i + count, size - i);
        }
        componentCount = newSize;
    }

註解@8.1.1 默認size=16,size >> 1 = 8 也就是擴容會以原大小一半的容量進行擴容。

註解@8.1.2 下面判斷根據場景通過Arrays.copyOf、System.arraycopy將Component數組擴容;插入尾部、中部、頭部等情況。本示例中沒有超過16,所以不會擴容,componentCount=2。

註解@8.2 循環拿出傳入的ByteBuf數組構建Component,並將其加入Component數組中;最後移動nextOffset。關於各個參數的含義,源碼給出了註釋。構造函數中

第一個參數:傳入的ByteBuf
第二個參數:源ByteBuf的readerIndex
第三個參數:unwrapped的buffer
第四個參數:unwrappedIndex
第五個參數:offset = components[cIndex - 1].endOffset
第六個參數:len = buf.readableBytes()  buf爲源buffer
第七個參數:slice = null (示例)

以endOffset爲例,等於插入數組中上一個Conponent的endOffset + 當前ByteBuf的可讀長度,從而維護了其在整個CompositeByteBuf的寫索引情況。

一個buffer對應一個Component,每個Component持有源buffer並維護了其在整個CompositeByteBuf的索引情況。

註解@9 設置整個CompositeByteBuf的讀索引和寫索引,讀索引初始值爲0;寫索引爲components[size - 1].endOffset,也就是整個Conponent數組中其每個元素維護的ByteBuf可讀字節(writerIndex - readerIndex)大小的總和。

final void setIndex0(int readerIndex, int writerIndex) {
 this.readerIndex = readerIndex;
 this.writerIndex = writerIndex;
}
public int capacity() {
  int size = componentCount;
  return size > 0 ? components[size - 1].endOffset : 0;
}

小結: CompositeByteBuf通過將多個ByteBuf裝入component數組中,對其統一維護讀寫索引,在外面看起來是一個統一的buffer;類似數據庫中的視圖。


本文分享自微信公衆號 - 瓜農老梁(gh_01130ae30a83)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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