Netty基礎系列(4) --堆外內存與零拷貝

前言

到目前爲止,我們知道Nio當中有三個最最核心的組件,分別是:Selelctor,Channel,Buffer。在Netty基礎系列(3) --徹底理解NIO 這一篇文章中只是進行了大致的介紹。

我們現在來深入理解一下Buffer在 堆內創建內存堆外創建內存 的底層原理,與 零拷貝 的具體實現。

Buffer

Buffer是一個抽象類,首先我們來看看Buffer有哪些實現類。

我們從上面這張截圖可以看出,Buffer的直接子類有7種。除了Java中Boolean類型。剩餘的7種基本類型都有與之對應的Buffer。不同類型的Buffer存儲的內容也不同,比如說ByteBuffer存儲的就是byte。IntBuffer存儲的就是int。不要想得太複雜,把底層想象成數組即可


接下來我們着重對ByteBuffer來進行講解。理解了一個其他的理解起來都差不多。

首先我們來看ByteBuffer的繼承關係圖

由上面的繼承關係圖可以看出,ByteBuffer的子類有五個,分別爲:

HeapByteBuffer:代表的是jvm堆內的緩存。
    HeapByteBufferR: 代表的是jvm堆內的只讀緩存。
MappedByteBuffer: 直接緩存的抽象基類。
    DirectByteBuffer: 代表的是操作系統內存的緩存。
        DirectByteBufferR: 代表的是操作系統內存的只讀緩存

上面這幾個類看名字和我的介紹我想你應該知道有什麼區別了,這裏其實只分爲兩大類。
分配在堆內存的緩存分配在操作系統內存的緩存

HeapByteBuffer

我們首先來看在堆內分配緩存的底層原理。

先來看一段代碼。

    public static void main(String args[]){
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    }

我們直接調用ByteBuffer的靜態方法創建了一個1024個字節的ByteBuffer緩存。那麼ByteBuffer的靜態方法allocate()在底層到底做了些什麼呢?

我們再來看看ByteBuffer類對於靜態方法allocate()的實現。

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }
}

沒錯,就是很簡單。直接new了一個HeapByteBuffer對象,並指定大小爲1024個字節。這裏暫時不用管capacity是什麼,後面我們會詳細的講解,在這裏capacity就是我們傳入的1024。

到目前爲止,我們已經創建了一個HeapByteBuffer對象。我們創建這個對象的意義就是用來對Channel進行讀寫。此時我們內存模型已經變成了如下圖所示:

對照着上圖我們再來看看之前寫的這個方法。

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

首先再棧空間的某個棧幀中創建了byteBuffer,接着將其指向堆內存中的對象HeapByteBuffer。

好了接下來是我們的重點!!!!

此時操作系統會自動在JVM之外的內存中分配一塊內存空間,這部分內存空間的創建和銷燬完全由操作系統來管理。我們無需在意。

Channel的數據無論是讀還是寫都是與操作系統分配的這塊內存打交道而不是我們的堆內存,當準備讀數據的時候,Channel將數據讀到操作系統分配的內存中,然後再複製到JVM堆內存中的HeapByteBuffer對象中。寫操作也是如此,當我們修改了HeapByteBuffer的數據,會將修改後的數據複製到操作系統分配的內存中,然後再寫到Channel中。

我們之前學的普通的IO操作底層基本上都是如此,我們思考一下,爲什麼不能直接將Channel懟到HeapByteBuffer中呢?

沒錯,如果你有一定的開發經驗,一定會想到垃圾回收器。當發送垃圾回收的時候,我們的對象在堆內存中是會發送移動的,移動後內存地址是會改變的,而io操作並不能追蹤到你改變後的內存地址。所以只能在jvm外分配內存來操作數據。因爲這一塊內存從創建到銷燬之間都是不會移動的。

DirectByteBuffer

我們來看看在堆外分配內存是如何實現的。

與前文一樣,我們首先來看在操作系統中直接分配內存的底層原理。先來看一段代碼。

    public static void main(String args[]){
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    }

與創建堆內緩存類似,我們直接調用ByteBuffer的靜態方法創建了一個1024個字節的DirectByteBuffer緩存。那麼ByteBuffer的靜態方法allocateDirect()方法與allocate()方法又有什麼區別呢?

我們再來看看ByteBuffer類對於靜態方法allocateDirect()的實現。

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
      public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
}

這裏也是直接new了一個DirectByteBuffer對象,我們進入該對象的構造函數看看幹了些什麼

這裏調用勒unsafe的allocateMemory(size)方法。我們進去後會發現這是一個native方法,底層調用的c語言的代碼。就是在操作系統內存中分配了一個我們指定大小的內存用以操作數據。並且記錄了這塊內存的地址。

此時我們的內存模型如下圖所示:

因爲內存中這塊內存不再是操作系統分配的,而是我們java代碼調用native方法,自己分配的內存,並且記錄了該內存的地址。所以我們操作數據就不需要再堆內操作可以直接在jvm內存以外的內存操作。此時每次讀寫操作都節省了兩次內存複製操作。

這就是我們大名鼎鼎的zero copy(零拷貝)技術。

總結

其實我們多思考一下,這樣的優勢大嗎?其實Channel中IO的操作相對於內存的複製來說是慢很多的,即便我們在讀寫數據的時候多了兩次複製的過程對於整體來說影響是不大的。

那麼什麼時候就會體現出零拷貝的優勢呢?有大量併發io操作,並且io操作是短暫完成的。這時由於節省了大量的內存copy操作,這些節省的時間積累下來也是非常可觀的。

netty的底層就是用的零拷貝技術,所以netty能做到很好併發,之後我們會分析在netty中零拷貝是如何落實的。

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