BIO、NIO、AIO 內部原理分析
NIO 之 Selector實現原理
NIO 之 Channel實現原理java
Java NIO 主要由下面3部分組成:數組
在傳統IO中,流是基於字節的方式進行讀寫的。
在NIO中,使用通道(Channel)基於緩衝區數據塊的讀寫。緩存
流是基於字節一個一個的讀取和寫入。
通道是基於塊的方式進行讀取和寫入。app
Buffer 的類結構圖以下:jvm
從圖中發現java中8中基本的類型,除了boolean外,其它的都有特定的Buffer子類。性能
每一個緩衝區都有這4個屬性,不管緩衝區是何種類型都有相同的方法來設置這些值測試
private int mark = -1; private int position = 0; private int limit; private int capacity;
初始值-1,表示未標記。
標記一個位置,方便之後reset從新從該位置讀取數據。this
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; }
緩衝區中讀取或寫入的下一個位置。這個位置從0開始,最大值等於緩衝區的大小spa
//獲取緩衝區的位置 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; }
//獲取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; }
緩衝區能夠保存元素的最大數量。該值在建立緩存區時指定,一旦建立完成後就不能修改該值。.net
//獲取緩衝區的容量 public final int capacity() { return capacity; }
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
public final Buffer rewind() { position = 0; mark = -1; return this; }
從源碼中發現,rewind修改了position和mark,而沒有修改limit。
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
從clear方法中,咱們發現Buffer中的數據沒有清空,若是經過Buffer.get(i)的方式仍是能夠訪問到數據的。若是再次向緩衝區中寫入數據,他會覆蓋以前存在的數據。
查看當前位置和limit之間的元素數。
public final int remaining() { return limit - position; }
判斷當前位置和limit之間是否還有元素
public final boolean hasRemaining() { return position < limit; }
ByteBuffer類結果圖
從圖中咱們能夠發現 ByteBuffer繼承於Buffer類,ByteBuffer是個抽象類,它有兩個實現的子類HeapByteBuffer和MappedByteBuffer類
HeapByteBuffer:在堆中建立的緩衝區。就是在jvm中建立的緩衝區。
MappedByteBuffer:直接緩衝區。物理內存中建立緩衝區,而不在堆中建立。
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); }
咱們發現allocate方法建立的緩衝區是建立的HeapByteBuffer實例。
HeapByteBuffer(int cap, int lim) { // package-private super(-1, 0, lim, cap, new byte[cap], 0); }
從堆緩衝區中看出,所謂堆緩衝區就是在堆內存中建立一個byte[]數組。
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
咱們發現allocate方法建立的緩衝區是建立的DirectByteBuffer實例。
DirectByteBuffer 構造方法
直接緩衝區是經過java中Unsafe類進行在物理內存中建立緩衝區。
public static ByteBuffer wrap(byte[] array) public static ByteBuffer wrap(byte[] array, int offset, int length);
能夠經過wrap類把字節數組包裝成緩衝區ByteBuffer實例。
這裏須要注意的的,把array的引用賦值給ByteBuffer對象中字節數組。若是array數組中的值更改,則ByteBuffer中的數據也會更改的。
ByteBuffer能夠轉換成其它類型的Buffer。例如CharBuffer、IntBuffer 等。
public ByteBuffer compact() { System.arraycopy(hb, ix(position()), hb, ix(0), remaining()); position(remaining()); limit(capacity()); discardMark(); return this; }
一、把緩衝區positoin到limit中的元素向前移動positoin位
二、設置position爲remaining()
三、 limit爲緩衝區容量
四、取消標記
例如:ByteBuffer.allowcate(10);
內容:[0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9]
[0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9]
pos=4
lim=10
cap=10
[4, 5, 6, 7, 8, 9, 6, 7, 8, 9]
pos=6
lim=10
cap=10
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
從結果中發現直接緩衝區建立分配空間比較耗時。
直接緩衝區比較適合讀寫操做,最好能重複使用直接緩衝區並屢次讀寫的操做。
堆緩衝區比較適合建立新的緩衝區,而且重複讀寫不會太多的應用。
建議:若是通過性能測試,發現直接緩衝區確實比堆緩衝區效率高才使用直接緩衝區,不然不建議使用直接緩衝區,由於JVM垃圾回收不會回收直接分配的物理內存,只回收虛擬機堆內存。