聊聊 Netty 零拷貝等技術(shù)對于內(nèi)存方面的優(yōu)化
Netty通過巧妙的內(nèi)存使用技巧盡可能節(jié)約內(nèi)存空間,進而減少java中Full gc的STW的時間,由此間接的提升了程序的性能,本文也將直接從源碼的角度分析一下Netty對于內(nèi)存方面的使用技巧,希望對你有所啟發(fā)。
使用基本類型替代包裝類
內(nèi)存空間算是寶貴的系統(tǒng)資源,為了提升CPU加載數(shù)據(jù)效率以及節(jié)約內(nèi)存空間,對于某些常見的基本數(shù)據(jù)類型,Netty都是能省則省,最直接的落地方案就是使用基本類型替代包裝類。
這其中totalPendingSize這個變量,它用于記錄那些待處理的數(shù)據(jù),為了節(jié)約內(nèi)存空間,記錄大小的類型是long而非Long,通過這種方式避免了創(chuàng)建java對象(java對象包含對象頭的信息,相比基本類型更占用內(nèi)存空間):
對此我們也給出這個變量的定義:
@SuppressWarnings("UnusedDeclaration")
private volatile long totalPendingSize;
又因為該字段需要保證線程安全,所以Netty設(shè)計者在此基礎(chǔ)上又將其設(shè)置為AtomicLong原子類型,通過static關(guān)鍵字加以修飾,使所有實例共享一個變量,從而避免沒必要的創(chuàng)建開銷和并發(fā)安全:
對此我們也給出源碼示例,即位于ChannelOutboundBuffer變量定義的位置:
//通過AtomicLongFieldUpdater修飾totalPendingSize
private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER =
AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");
動態(tài)內(nèi)存調(diào)整
除上述內(nèi)存使用技巧以外,netty在進行內(nèi)存分配時也用到的動態(tài)調(diào)整的使用技巧,該設(shè)計理念比較簡單,按照空間與分配思想:后續(xù)使用的內(nèi)存大小大概率是等同于本次使用的空間大小,所以Netty在調(diào)用record進行內(nèi)存分配時,如果發(fā)現(xiàn)縮小空間依然可以滿足要求,則進行縮容,反之進行擴容,由此得到一個盡可能節(jié)約內(nèi)存空間且能滿足業(yè)務(wù)要求的數(shù)值:
private void record(int actualReadBytes) {
//若實際需要的空間 <= 預(yù)縮小達到的尺寸,則對nextReceiveBufferSize進行縮減
if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
if (decreaseNow) {
index = max(index - INDEX_DECREMENT, minIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
} else {
decreaseNow = true;
}
} else if (actualReadBytes >= nextReceiveBufferSize) {//如果所需空間大于nextReceiveBufferSize,則進行擴容
index = min(index + INDEX_INCREMENT, maxIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
}
}
應(yīng)用層面的zero-copy
內(nèi)存拷貝也是存在一定的時間開銷,例如我們現(xiàn)在有一個字符串的數(shù)據(jù)需要將byte1和byte2拼接起來才能得到,按照傳統(tǒng)的實現(xiàn)思路,我們需要開發(fā)一個足夠容納byte1和byte2的內(nèi)存空間,然后將byte1和byte2一并寫入,這種做法有著如下耗時點:
- 開辟內(nèi)存空間所占用的時間。
- 將byte1內(nèi)存新開辟空間的耗時。
- 將byte2寫入新開辟的內(nèi)存空間耗時。
而Netty則不是這樣做,它的設(shè)計思路是直接將兩個數(shù)組,邏輯上組合,即通過一個數(shù)組指向這兩個引用,從邏輯上視為一個整體,而不是物理操作上的組合:
對此我們給出CompositeByteBuf的addComponent0方法,可以看到對于需要組合的數(shù)據(jù)buffer,它會通過addComp方法將這個ByteBuf 存到CompositeByteBuf底層的數(shù)組中,由此保證數(shù)據(jù)邏輯上的一致:
private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) {
assert buffer != null;
boolean wasAdded = false;
try {
checkComponentIndex(cIndex);
//將其包裝為Component
Component c = newComponent(ensureAccessible(buffer), 0);
int readableBytes = c.length();
//......
//添加到CompositeByteBuf底層的components數(shù)組中,通過邏輯完成組合
addComp(cIndex, c);
//......
return cIndex;
} finally {
//......
}
}
//添加到components數(shù)組中保證邏輯上的一致
private void addComp(int i, Component c) {
//......
components[i] = c;
}
使用堆外內(nèi)存
將數(shù)據(jù)存放在JVM非堆內(nèi)存空間,通過減少沒必要的GC確保操作和執(zhí)行性能的高效,這也是Netty中對于內(nèi)存方面的優(yōu)化,這其中最經(jīng)典的就是PooledHeapByteBuf,它直接操作的就是堆外內(nèi)存的數(shù)據(jù):
對此我們也給處PooledDirectByteBuf 獲取直接內(nèi)存的源碼實現(xiàn):
//從內(nèi)存池中獲取直接內(nèi)存空間返回給用戶使用
static PooledDirectByteBuf newInstance(int maxCapacity) {
PooledDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
需要補充的是,這種做法也存在的一定的風險:
- 創(chuàng)建速度慢。
- 存放在非堆內(nèi)存空間,使用不當可能造成內(nèi)存泄漏。
內(nèi)存池化復(fù)用
上文的堆內(nèi)存就是PooledHeapByteBuf即池化過的內(nèi)存,通過池化:
- 保證對象復(fù)用,減小沒必要的創(chuàng)建開銷。
- 提升程序并發(fā)執(zhí)行性能。
對此我們給出相應(yīng)的源碼實現(xiàn):
//初始化直接內(nèi)存池化工廠RECYCLER
private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
new ObjectCreator<PooledDirectByteBuf>() {
@Override
public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
return new PooledDirectByteBuf(handle, 0);
}
});
//從內(nèi)存池中獲取直接內(nèi)存空間返回給用戶使用
static PooledDirectByteBuf newInstance(int maxCapacity) {
//從內(nèi)存池中獲取直接內(nèi)存空間
PooledDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
對jdk零拷貝的封裝
我們在上述所講的零復(fù)制更多強調(diào)的是應(yīng)用層面上的零復(fù)制,也就是通過減少應(yīng)用層面上數(shù)據(jù)的拷貝提升程序的執(zhí)行效率。實際上Netty也有基于操作系統(tǒng)層面的零拷貝實現(xiàn),這其中最典型的實現(xiàn)就是DefaultFileRegion的transferTo函數(shù),它底層調(diào)用JDK自帶的NIO零拷貝方法transferTo實現(xiàn)當前文件數(shù)據(jù)通過sendfile調(diào)用傳輸?shù)絪ocket通道中,由此避免數(shù)據(jù)傳輸時多次切態(tài)、內(nèi)核緩沖區(qū)和用戶緩沖區(qū)來回拷貝的開銷:
對此我們也給出DefaultFileRegion類中transferTo的源碼,可以看到其底層就是將JDK默認的NIO零拷貝方法進行封裝,將DefaultFileRegion封裝的FileChannel 的文件數(shù)據(jù)拷貝到target的文件通道中,其底層就用到內(nèi)核函數(shù)sendfile:
private FileChannel file;
@Override
public long transferTo(WritableByteChannel target, long position) throws IOException {
//......
long written = file.transferTo(this.position + position, count, target);
if (written > 0) {
transferred += written;
} else if (written == 0) {
//......
}
return written;
}