前言
Java NIO 中的三件法寶:Channel
、Selector
和 Buffer
。前面幾節中,我們花了很大篇幅講過 Selector
,咱們今天只搞 Buffer
。希望能通過本文搞明白 Buffer
的基本用法和原理。
掌握重點:
-
兩個重要指針不停變換
-
一塊
Buffer
可讀可寫 -
基本操作的 api 用法
-
ByteBuffer
可以在 JVM 堆外分配直接內存
基本操作
上一篇我們模擬 client 發送請求的時候代碼如下:
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
System.out.printf("接到服務端響應:%s,處理了%d\r\n", br.readLine(), (System.currentTimeMillis() - start));
br.close();
inputStream.close();
在普通 BIO 模式下,我們只能自己維護一個 byte 數組或者是 char 數組來進行批量讀寫,或者使用 BufferedReader
和 BufferedInputStream
來做讀寫緩衝區。
buffer.clear();
buffer.put(("收到,你發來的是:" + sb + "\r\n").getBytes("utf-8"));
buffer.flip();
Java NIO Buffer 用於和 NIO Channel 交互,我們從Channel
中讀取數據到 Buffer
裏,從 Buffer
把數據寫入到 Channel
。本質上,就是存在一塊內存區,可以用來寫入數據,並在稍後讀取出來。這塊內存被 NIO Buffer 包裹起來,對外提供一系列的讀寫方便開發的接口。
-
把數據寫入
Buffer
; -
調用 flip();
-
從
Buffer
中讀取數據; -
調用 clear() 或者 compact()
當寫入數據到 Buffer
中時,Buffer
會記錄已經寫入的數據大小。當需要讀數據時,通過 flip() 方法把 Buffer
從寫模式調整爲讀模式;在讀模式下,可以讀取所有已經寫入的數據。
Buffer實現
緩存區,內部使用字節數組存儲數據,並維護幾個特殊變量,實現數據的反覆利用。在 java.nio.Buffer 中定義了4個成員變量:
-
mark:初始值爲 -1,用於備份當前的 position ;
-
position:初始值爲 0,position 表示當前可以寫入或讀取數據的位置,當寫入或讀取一個數據後,position 向前移動到下一個位置;
-
limit:寫模式下,limit 表示最多能往 Buffer 裏寫多少數據,等於 capacity 值;讀模式下,limit 表示最多可以讀取多少數據。
-
capacity:緩存數組大小
核心點:對於 Buffer 的操作,就是在不停的變換 position 和 limit 指針的位置,達到定位讀取位置和終止位置的目的,從而可以準確的在邊界內讀取數據。
代碼實現:
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
以字節緩衝區爲例,ByteBuffer
是一個抽象類,不能直接通過 new 語句來創建,只能通過一個 static 方法 allocate 來創建:
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
調用上述語句,相當於創建一個大小爲 10 個字節的 ByteBuffer
,此時 mark = -1, position = 0, limit = 10, capacity = 10
我們看一下 Buffer
的常見方法,內部是如何實現的:
put
put 方法是把一個 byte 變量 x 放到緩衝區中,同時 position 會加 1
public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
final int nextPutIndex() { // package-private
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
一起看一下不停 put 數據時,幾個變量的變化:
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put((byte) 'l');
byteBuffer.put((byte) 'o');
byteBuffer.put((byte) 'v');
byteBuffer.put((byte) 'e');
System.out.println(byteBuffer.limit()); // 結果10
System.out.println(byteBuffer.position());// 結果4
System.out.println(byteBuffer.capacity());// 結果10
byteBuffer.put((byte) ' ');
byteBuffer.put((byte) 'x');
byteBuffer.put((byte) 'y');
byteBuffer.put((byte) 'j');
System.out.println(byteBuffer.limit());// 結果10
System.out.println(byteBuffer.position());// 結果8
System.out.println(byteBuffer.capacity());// 結果10
get
get 方法,是從 position 的位置去取緩衝區中的一個字節
public byte get() {
return hb[ix(nextGetIndex())];
}
final int nextGetIndex() { // package-private
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
flip
如果想在一個 Buffer
中放入了數據,然後想從中讀取的話,就要把 position 調到我想讀的那個位置纔行,同時需要調整 limit。
byteBuffer.limit(byteBuffer.position())
byteBuffer.position(0);
Java 中把這兩步操作,封裝在一個 flip 方法中:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
mark
mark 就很容易理解了,它就是記住當前的位置用的
public final Buffer mark() {
mark = position;
return this;
}
在調用過 mark 以後,再進行緩衝區的讀寫操作,position 就會發生變化,爲了再回到當初的位置,我們可以調用 reset 方法恢復position 的值:
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
clear
把 Buffer
中特殊的4個變量初始爲原始值
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
回顧一下核心點:對於 Buffer 的操作,就是在不停的變換 position 和 limit 指針的位置,達到定位讀取位置和終止位置的目的,從而可以準確的在邊界內讀取數據。
Direct Buffer
在創建 ByteBuffer
是我們是採用的靜態方法直接 allocate 得到一個 buffer 對象:
ByteBuffer buf = ByteBuffer.allocate(1024);
在 JVM 中,創建的對象是放入在堆中。比如,當我們 Object o = new Object()
時,會在堆內存上分配一塊內存空間給 new Object()
,在棧空間上持有引用 o
保存 Object
的內存地址 。JVM 做垃圾回收,會把堆中的對象,在不同的分區中來回拷貝。內存地址會頻繁發生變化,本身 Buffer
會頻繁讀寫,這樣會導致內存整理繁瑣。有沒有辦法脫離JVM對象管理呢?在創建 Buffer
的靜態方法中還有一個方法:
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
我們來比對一下方法的實現:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
調用 allocate() 創建了一個 HeapByteBuffer
,調用 allocateDirect() 創建的是 DirectByteBuffer
。看名字很直觀的表達,一個是「堆」內存,一個是「直接」內存。
看一下 DirectByteBuffer
的實現:
// Primary constructor
//
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
這裏最重要的就是使用了 unsafe.allocateMemory 來分配內存,而 allocateMemory 是一個 native 方法,會調用 malloc 方法在 JVM 外分配一塊內存空間。
總之,這裏在 Java 堆外申請了一塊內存,並把這個內存的地址記錄下來。以後要是再使用這個ByteBuffer
的話,就會直接訪問從address開始的那一段內存。
DirectBuffer
一個直觀的優點是不被 GC 管理,所以發生 GC 的時候,整理內存的壓力就會小。當然,它並不是完全不被 GC 管理還是能被回收的,但是在 GC 平常整理內存的時候確實是不會去管它。
類結構
我們只是以常見的 ByteBuffer
爲例,在 NIO 中還提供了各種類型的Buffer ,這裏就不再贅述。
結論
-
Buffer
中有兩個重要指針 position 和 limit 不停變換位置 -
一塊
Buffer
可讀可寫,內部是一個 capacity 大小的數組 -
基本操作的 api 用法,put 、get、flip、mark、clear
-
flip 方法改變了指針 position 和 limit 的位置
-
可以在 JVM 堆外分配直接內存
今天就搞到的這裏,劃的重點需要牢記,Buffer
的操作不注意順序會出現各種問題。
系列
關注我
如果您在微信閱讀,請您點擊鏈接 關注我 ,如果您在 PC 上閱讀請掃碼關注我,歡迎與我交流隨時指出錯誤