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中的對象引用計數器,用於跟蹤對象的引用和銷燬,做自動內存回收。
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的大小是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是通過維護塊的使用狀態標識來實現。