聲明:java
本文由Yasin Shaw原創,首發於我的網站yasinshaw.com和公衆號"xy的技術圈"。git
若是須要轉載請聯繫我(微信:yasinshaw)並在文章開頭顯著的地方註明出處。github
關注公衆號便可獲取學習資源或加入技術交流羣。數組
Buffer是「緩衝區」的意思。在Java NIO中,全部的數據都要通過Buffer,下圖是Buffer內部的基本結構。微信
它其實就是一個數組,裏面有三個指針:position, limit, capacity。socket
capacity
爲這個數組的容量,是不可變的。學習
limit
是Buffer中第一個不可讀寫的元素的下標,也即limit後的數據不可進行讀寫。limit不能爲負,也不能大於capacity。網站
limit初始的時候是與capacity值是同樣的。this
position
表示下一個元素即將讀或者寫的下標。position不能爲負也不能大於limit。position初始的時候爲0。spa
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用的是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字節,那寫入數據之後三個指針就是這樣的:
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讀到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還有其它一些操做那三個指針的方法,不過使用頻率沒有上述方法高,因此本文不作詳細介紹,感興趣的讀者能夠去看一下源碼。
這裏貼一下讀和寫的使用的案例代碼:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 從字符串寫到Buffer
buffer.put("hi, 這是client".getBytes(StandardCharsets.UTF_8));
buffer.flip(); // 轉換模式
// 從Buffer寫到Channel
socketChannel.write(buffer);
複製代碼
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);
}
複製代碼