Java NIO - Buffer

聲明:java

本文由Yasin Shaw原創,首發於我的網站yasinshaw.com和公衆號"xy的技術圈"。git

若是須要轉載請聯繫我(微信:yasinshaw)並在文章開頭顯著的地方註明出處github

關注公衆號便可獲取學習資源或加入技術交流羣數組

結構

Buffer是「緩衝區」的意思。在Java NIO中,全部的數據都要通過Buffer,下圖是Buffer內部的基本結構。微信

buffer

它其實就是一個數組,裏面有三個指針:position, limit, capacity。socket

capacity

capacity爲這個數組的容量,是不可變的。學習

limit

limit是Buffer中第一個不可讀寫的元素的下標,也即limit後的數據不可進行讀寫。limit不能爲負,也不能大於capacity。網站

limit初始的時候是與capacity值是同樣的。this

position

position表示下一個元素即將讀或者寫的下標。position不能爲負也不能大於limit。position初始的時候爲0。spa

類關係

Buffer是一個抽象類,它有許多子抽象類,對應7種Java的基本類型(除了boolean)。以下圖:

image.png

ByteBuffer爲例,它有兩種實現,一種是HeapByteBuffer,另外一種是DirectByteBuffer,分別對應堆內存直接內存

堆內存會把這個對象分配在JVM堆裏,就跟普通對象同樣。而直接內存又被稱爲堆外內存,在使用IO的時候,咱們更推薦使用直接內存。

爲何推薦使用直接內存呢?其實這跟JVM的垃圾回收機制有關。IO每每會佔用一個比較大的內存空間,若是分配到JVM堆裏面,會被認爲是一個大對象,影響JVM垃圾回收效率。

堆外內存若是滿了(達到系統內存的界限),也會拋出OOM異常。

初始化

Buffer有什麼用?Buffer通常是與Channel配合起來用,Channel讀數據的時候,會先讀到Buffer裏,寫數據的時候,也會先寫到Buffer裏。

下面介紹一下具體是怎麼使用Buffer的。

通常來講,是直接使用第二級類,好比ByteBuffer。它們有兩個工廠方法allocateallocateDirect,用於初始化和申請內存。前面提到了在操做IO時,一般使用直接內存,因此通常是這樣初始化:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
複製代碼

能夠用isDirect()方法來判斷當前Buffer對象是否使用了直接內存。

寫數據

往Buffer中寫數據主要有兩種方式:

  • 從Channel寫到Buffer
  • 從數組寫到Buffer

從Channel寫到Buffer用的是Channel的read(Buffer buffer)方法,而從數組寫到Buffer,主要用的是Buffer的put方法。

// 獲取Channel裏面的數據並寫到buffer
// 返回的是讀的位置,也就是buffer的position
int readBytes = socketChannel.read(buffer);

// 從byte數組寫到Buffer
buffer.put("hi, 這是client".getBytes(StandardCharsets.UTF_8));
複製代碼

咱們假設Buffer申請了1024字節,這個字符串佔用16字節,那寫入數據之後三個指針就是這樣的:

  • position = 16
  • limit = 1024
  • capacity = 1024

切換模式

Buffer分爲讀模式和寫模式,能夠經過flip()方法轉換模式。事實上,查看這個方法源碼,發現flip方法也只是對三個指針進行了操做而已。

public Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
複製代碼

mark指針用於reset()方法,若是reset()方法被調用,position就會被重置到mark位置。若是mark沒有被定義,調用reset()方法會拋出InvalidMarkException異常。一旦mark被定義,就必定不能爲負數,而且小於等於position的位置。

mark()方法的做用至關於能夠「暫時記錄position」的位置,這樣之後能夠經過reset()方法回到這個位置。

切換模式後,三個指針變成了這樣:

  • position = 0
  • limit = 16
  • capacity = 1024

讀數據

與寫數據對應,讀數據也有兩種方式:

  • 從Buffer讀到Channel
  • 從Buffer讀到數組

讀數據會從position讀到limit的位置。

示例代碼:

// 讀取buffer的數據並寫入channel
socketChannel.write(buffer);

// 把buffer裏面的數據讀到byte數組
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, StandardCharsets.UTF_8);
複製代碼

這裏用到了Buffer的remaining()方法。這個方法是告訴咱們須要讀多少字節,方法源碼:

public final int remaining() {
    return limit - position;
}
複製代碼

清空

通常來講,一個Channel用一個Buffer,但Buffer能夠重複使用,尤爲是對於一些比較大的IO傳輸內容來講(好比文件),clear()compact()方法能夠重置Buffer。它們有一些微小的區別。

對於clear方法來講,position將被設回0,limit被設置成 capacity的值。

compact方法將全部未讀的數據拷貝到Buffer起始處。而後將position設到最後一個未讀元素後面一位。limit屬性依然像clear方法同樣,設置成capacity。如今Buffer準備好寫數據了,可是不會覆蓋未讀的數據。

通常來講,用clear方法的場景會多一點。

源碼:

public Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

public ByteBuffer compact() {
    int pos = position();
    int lim = limit();
    assert (pos <= lim);
    int rem = (pos <= lim ? lim - pos : 0);
    try {
        UNSAFE.copyMemory(ix(pos), ix(0), (long)rem << 0);
    } finally {
        Reference.reachabilityFence(this);
    }
    position(rem);
    limit(capacity());
    discardMark();
    return this;
}
複製代碼

Buffer還有其它一些操做那三個指針的方法,不過使用頻率沒有上述方法高,因此本文不作詳細介紹,感興趣的讀者能夠去看一下源碼。

使用

這裏貼一下讀和寫的使用的案例代碼:

從字符串到Channel:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 從字符串寫到Buffer
buffer.put("hi, 這是client".getBytes(StandardCharsets.UTF_8));
buffer.flip(); // 轉換模式
// 從Buffer寫到Channel
socketChannel.write(buffer);
複製代碼

從Channel到字符串:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 從Channel寫到Buffer
int readBytes = socketChannel.read(buffer);
if (readBytes > 0) {
    buffer.flip(); // 轉換模式
    byte[] bytes = new byte[buffer.remaining()];
    // 從Buffer寫到字節數組
    buffer.get(bytes);
    String body = new String(bytes, StandardCharsets.UTF_8);
    System.out.println("server 收到:" + body);
}
複製代碼

完整示例代碼

相關文章
相關標籤/搜索