前言
本文通過分析抽象內存分配器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) 由實現類決定堆內存或者堆外內存 |
下面走查下抽象內存分配器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(this, false, 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,先直觀感受下。
示例代碼
@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(false, 0, 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源創計劃”,歡迎正在閱讀的你也加入,一起分享。