Buffer是“緩衝區”的意思。在Java NIO中,所有的資料都要經過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
)。如下圖:
以ByteBuffer
為例,它有兩種實現,一種是HeapByteBuffer
,另一種是DirectByteBuffer
,分別對應堆記憶體和直接記憶體。
堆記憶體會把這個物件分配在JVM堆裡,就跟普通物件一樣。而直接記憶體又被稱為堆外記憶體,在使用IO的時候,我們更推薦使用直接記憶體。
為什麼推薦使用直接記憶體呢?其實這跟JVM的垃圾回收機制有關。IO往往會佔用一個比較大的記憶體空間,如果分配到JVM堆裡面,會被認為是一個大物件,影響JVM垃圾回收效率。
堆外記憶體如果滿了(達到系統記憶體的界限),也會丟擲OOM異常。
Buffer有什麼用?Buffer一般是與Channel配合起來用,Channel讀資料的時候,會先讀到Buffer裡,寫資料的時候,也會先寫到Buffer裡。
下面介紹一下具體是怎麼使用Buffer的。
一般來說,是直接使用第二級類,比如ByteBuffer
。它們有兩個工廠方法allocate
和allocateDirect
,用於初始化和申請記憶體。前面提到了在操作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 協議》,轉載必須註明作者和本文連結