NIO學習筆記一之Buffer
有一些個人理解,大家辯證地看,有問題的地方,還請大家指出。
Java NIO中的Buffer用於和NIO通道進行互動。如你所知,資料是從通道讀入緩衝區,從緩衝區寫入到通道中的。
緩衝區本質上是一塊可以寫入資料,然後可以從中讀取資料的記憶體。這塊記憶體被包裝成NIO Buffer物件,並提供了一組方法,用來方便的訪問該塊記憶體。
1.Buffer的基本用法
使用Buffer讀寫資料一般遵循以下幾個步驟:
- 向Buffer寫資料
- 呼叫filp()方法,該方法的作用是將Buffer從寫模式切換到讀模式,後面會具體介紹
- 從Buffer中讀取資料
- 呼叫clear()方法或是compact()方法,這兩個方法的作用主要是清空緩衝區,其實並不是真的清空緩衝區的資料,只是更改了幾個標誌位,後面會具體介紹
一個栗子:
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.put((byte)4);
buffer.flip();
for(int i=0;i<buffer.limit();i++){
System.out.println(buffer.get());
}
Buffer的幾個重要成員變數
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
capacity:
字面意思理解,這個變數指的是容量,我們可以把它理解為Buffer的容量,也就是我們最多可以給Buffer寫入capacity大小的資料
position:
position表示當前讀取(或是寫入)的位置,寫入(或是讀取)一個數據後,position會指向下一個可讀(或是可寫)的位置。
limit:
limit表示我們最多可以讀到(或寫到)limit位置,這個變數容易和capacity混淆。
這個變量出現的原因主要是我們讀和寫都用的是一塊記憶體,所以需要一個標記,來只是我們最多可以讀到(或寫到)什麼位置。
mark:
這個變數是一個記號,用來記住當前的position,之後介紹mark()方法的時候會具體介紹。
3.Buffer的型別
Buffer主要有以下一種型別,我們主要會以ByteBuffer為主介紹。
4.Buffer的分配
堆中分配:
ByteBuffer buffer = ByteBuffer.allocate(48);
我們來看一下allocate(int capacity)的程式碼:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
我們可以看到,首先做了基本的校驗,然後就return了一個HeapByteBuffer的物件。
我們繼續看一下HeapByteBuffer的構造方法:
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
}
我們可以看到,該構造方法中直接呼叫了父類(也就是ByteBuffer類)的構造方法:
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
我們可以看到:這兩個方法都是包訪問許可權,也就是隻有java.nio包中的類可以呼叫(直白地說,就是我們是不能直接呼叫的),所以這個allocate(int capacity)是一個工廠方法!
ByteBuffer中呼叫的是HeapByteBuffer的構造方法,也是就在JVM堆中分配記憶體,所以我們呼叫allocate(int capacity)得到的是普通的物件。
直接記憶體中分配:
我們還可以在直接記憶體中分配ByteBuffer物件:
ByteBuffer buffer = ByteBuffer.allocateDirect(48);
我們來看一下是怎麼分配的:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
再點進去看一下DirectByteBuffer(int cap):
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(long size)來分配記憶體的,我們由unsafe這個名字就可以知道,它是危險的,它使JVM 和作業系統核心可以交流。
unsafe的allocateMemory(long size)是一個本地方法,我們暫時看不到了。
但是,我們可以知道,allocateMemory(long size)分配的是直接記憶體,也就是JVM堆外記憶體,而allocate(int capacity)分配的是堆內記憶體。
為什麼要支援分配直接記憶體呢?這個問題從網路分層來說起吧:
我們的TCP/IP模型,將網路分為了幾大層:應用層、傳輸層、網路層和鏈路層。其中,傳輸層(包括傳輸層)以下是執行在核心空間的,應用層是執行在使用者空間的。我們要用通過網路接收資料,要將核心空間的資料複製要使用者空間;同樣地,要通過網路傳送資料,要將使用者空間的資料複製到核心空間,再進行傳送。
這樣複製來複制去效率未免很低,於是我們可以通過直接分配直接記憶體的方式,來減少一次複製。
但是這個直接記憶體用起來是有一些注意事項的,後面我會專門寫一篇文章來總結這個。
4.幾個方法介紹
flip()方法
flip方法將Buffer從寫模式切換到讀模式。呼叫flip()方法會將position設回0,並將limit設定成之前position的值。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
position為寫的資料的位置,表示就寫了這麼多的資料,我們讀當然不能超過它啦,於是就將limit設定為position,我們讀要從頭開始讀,就將position置為0。
rewind()方法
Buffer.rewind()將position設回0,所以你可以重讀Buffer中的所有資料。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear()方法
一旦讀完Buffer中的資料,需要讓Buffer準備好再次被寫入。可以通過clear()來完成。
clear()方法將position置為0,limit置為capacity,mark置為-1,也就是將一切置為了原始狀態。我們可以看到,它並沒有真正地清空緩衝區的資料,只是將標誌位還原。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
compact()方法
compact()方法將所有未讀的資料拷貝到Buffer起始處。然後將position設到最後一個未讀元素的後面。
mark()方法
這個方法是與下面的reset()方法配套使用的,這個方法很簡單,只是將當前的position記錄在mark上
public final Buffer mark() {
mark = position;
return this;
}
reset()方法
這個方法就是將position恢復為之間記錄下來的值
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();//必要的校驗
position = m;
return this;
}