NIO 之 ByteBuffer實現原理
前言
Java NIO 主要由下面3部分組成:
- Buffer
- Channel
- Selector
在傳統IO中,流是基於位元組的方式進行讀寫的。 在NIO中,使用通道(Channel)基於緩衝區資料塊的讀寫。
流是基於位元組一個一個的讀取和寫入。 通道是基於塊的方式進行讀取和寫入。
Buffer 類結構圖
Buffer 的類結構圖如下:
Buffer類結構圖
從圖中發現java中8中基本的型別,除了boolean外,其它的都有特定的Buffer子類。
Buffer類分析
Filed
每個緩衝區都有這4個屬性,無論緩衝區是何種型別都有相同的方法來設定這些值
private int mark = -1; private int position = 0; private int limit; private int capacity;
1. 標記(mark)
初始值-1,表示未標記。 標記一個位置,方便以後reset重新從該位置讀取資料。
public final Buffer mark() {
mark = position;
return this;
}
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
2. 位置(position)
緩衝區中讀取或寫入的下一個位置。這個位置從0開始,最大值等於緩衝區的大小
//獲取緩衝區的位置 public final int position() { return position; } //設定緩衝區的位置 public final Buffer position(int newPosition) { if ((newPosition > limit) || (newPosition < 0)) throw new IllegalArgumentException(); position = newPosition; if (mark > position) mark = -1; return this; }
3. 限度(limit)
//獲取limit位置
public final int limit() {
return limit;
}
//設定limit位置
public final Buffer limit(int newLimit) {
if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}
4. 容量(capacity)
緩衝區可以儲存元素的最大數量。該值在建立快取區時指定,一旦建立完成後就不能修改該值。
//獲取緩衝區的容量
public final int capacity() {
return capacity;
}
filp 方法
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
- 將limit設定成當前position的座標
- 將position設定為0
- 取消標記
rewind 方法
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
從原始碼中發現,rewind修改了position和mark,而沒有修改limit。
- 將position設定為0
- 取消mark標記
clear 方法
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
- 將position座標設定為0
- limit設定為capacity
- 取消標記
從clear方法中,我們發現Buffer中的資料沒有清空,如果通過Buffer.get(i)的方式還是可以訪問到資料的。如果再次向緩衝區中寫入資料,他會覆蓋之前存在的資料。
remaining 方法
檢視當前位置和limit之間的元素數。
public final int remaining() {
return limit - position;
}
hasRemaining 方法
判斷當前位置和limit之間是否還有元素
public final boolean hasRemaining() {
return position < limit;
}
ByteBuffer 類分析
ByteBuffer類結果圖
從圖中我們可以發現 ByteBuffer繼承於Buffer類,ByteBuffer是個抽象類,它有兩個實現的子類HeapByteBuffer和MappedByteBuffer類
HeapByteBuffer:在堆中建立的緩衝區。就是在jvm中建立的緩衝區。 MappedByteBuffer:直接緩衝區。實體記憶體中建立緩衝區,而不在堆中建立。
allocate 方法(建立堆緩衝區)
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
我們發現allocate方法建立的緩衝區是建立的HeapByteBuffer例項。
HeapByteBuffer 構造
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
}
從堆緩衝區中看出,所謂堆緩衝區就是在堆記憶體中建立一個byte[]陣列。
allocateDirect建立直接緩衝區
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
我們發現allocate方法建立的緩衝區是建立的DirectByteBuffer例項。
DirectByteBuffer構造
DirectByteBuffer 構造方法
直接緩衝區是通過java中Unsafe類進行在實體記憶體中建立緩衝區。
wrap 方法
public static ByteBuffer wrap(byte[] array)
public static ByteBuffer wrap(byte[] array, int offset, int length);
可以通過wrap類把位元組陣列包裝成緩衝區ByteBuffer例項。 這裡需要注意的的,把array的引用賦值給ByteBuffer物件中位元組陣列。如果array陣列中的值更改,則ByteBuffer中的資料也會更改的。
get 方法
- public byte get() 獲取position座標元素,並將position+1;
- public byte get(int i) 獲取指定索引下標的元素
- public ByteBuffer get(byte[] dst) 從當前position中讀取元素填充到dst陣列中,每填充一個元素position+1;
- public ByteBuffer get(byte[] dst, int offset, int length) 從當前position中讀取元素到dst陣列的offset下標開始填充length個元素。
put 方法
- public ByteBuffer put(byte x) 寫入一個元素並position+1
- public ByteBuffer put(int i, byte x) 指定的索引寫入一個元素
- public final ByteBuffer put(byte[] src) 寫入一個自己陣列,並position+陣列長度
- public ByteBuffer put(byte[] src, int offset, int length) 從一個自己陣列的offset開始length個元素寫入到ByteBuffer中,並把position+length
- public ByteBuffer put(ByteBuffer src) 寫入一個ByteBuffer,並position加入寫入的元素個數
檢視緩衝區
Paste_Image.png
ByteBuffer可以轉換成其它型別的Buffer。例如CharBuffer、IntBuffer 等。
壓縮緩衝區
public ByteBuffer compact() {
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
return this;
}
1、把緩衝區positoin到limit中的元素向前移動positoin位 2、設定position為remaining() 3、 limit為緩衝區容量 4、取消標記
例如:ByteBuffer.allowcate(10); 內容:[0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9]
compact前
[0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9] pos=4 lim=10 cap=10
compact後
[4, 5, 6, 7, 8, 9, 6, 7, 8, 9] pos=6 lim=10 cap=10
slice方法
public ByteBuffer slice() {
return new HeapByteBuffer(hb,
-1,
0,
this.remaining(),
this.remaining(),
this.position() + offset);
}
建立一個分片緩衝區。分配緩衝區與主緩衝區共享資料。 分配的起始位置是主緩衝區的position位置 容量為limit-position。 分片緩衝區無法看到主緩衝區positoin之前的元素。
直接緩衝區和堆緩衝區效能對比
下面我們從緩衝區建立的效能和讀取效能兩個方面進行效能對比。
讀寫效能對比
public static void directReadWrite() throws Exception {
int time = 10000000;
long start = System.currentTimeMillis();
ByteBuffer buffer = ByteBuffer.allocate(4*time);
for(int i=0;i<time;i++){
buffer.putInt(i);
}
buffer.flip();
for(int i=0;i<time;i++){
buffer.getInt();
}
System.out.println("堆緩衝區讀寫耗時 :"+(System.currentTimeMillis()-start));
start = System.currentTimeMillis();
ByteBuffer buffer2 = ByteBuffer.allocateDirect(4*time);
for(int i=0;i<time;i++){
buffer2.putInt(i);
}
buffer2.flip();
for(int i=0;i<time;i++){
buffer2.getInt();
}
System.out.println("直接緩衝區讀寫耗時:"+(System.currentTimeMillis()-start));
}
輸出結果:
堆緩衝區建立耗時 :70
直接緩衝區建立耗時:47
從結果中我們發現堆緩衝區讀寫比直接緩衝區讀寫耗時更長。
public static void directAllocate() throws Exception {
int time = 10000000;
long start = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
ByteBuffer buffer = ByteBuffer.allocate(4);
}
System.out.println("堆緩衝區建立時間:"+(System.currentTimeMillis()-start));
start = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
ByteBuffer buffer = ByteBuffer.allocateDirect(4);
}
System.out.println("直接緩衝區建立時間:"+(System.currentTimeMillis()-start));
}
輸出結果:
堆緩衝區建立時間:73
直接緩衝區建立時間:5146
從結果中發現直接緩衝區建立分配空間比較耗時。
對比結論
直接緩衝區比較適合讀寫操作,最好能重複使用直接緩衝區並多次讀寫的操作。 堆緩衝區比較適合建立新的緩衝區,並且重複讀寫不會太多的應用。
建議:如果經過效能測試,發現直接緩衝區確實比堆緩衝區效率高才使用直接緩衝區,否則不建議使用直接緩衝區。
作者:jijs 連結:https://www.jianshu.com/p/451cc865d413 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。