Java NIO - Buffer

LZC發表於2020-06-16

Buffer是“緩衝區”的意思。在Java NIO中,所有的資料都要經過Buffer,下圖是Buffer內部的基本結構。

buffer

它其實就是一個陣列,裡面有三個指標:position, limit, capacity。

capacity

capacity為這個陣列的容量,是不可變的。

limit

limit是Buffer中第一個不可讀寫的元素的下標,也即limit後的資料不可進行讀寫。limit不能為負,也不能大於capacity。

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

position

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

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);
}
預覽

完整示例程式碼

作者:公眾號_xy的技術圈

連結:www.imooc.com/article/289086

來源:慕課網

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章