NIO 之 ByteBuffer實現原理

相關文章

BIO、NIO、AIO 內部原理分析
NIO 之 Selector實現原理
NIO 之 Channel實現原理java

前言

Java NIO 主要由下面3部分組成:數組

  • Buffer
  • Channel
  • Selector

在傳統IO中,流是基於字節的方式進行讀寫的。
在NIO中,使用通道(Channel)基於緩衝區數據塊的讀寫。緩存

流是基於字節一個一個的讀取和寫入。
通道是基於塊的方式進行讀取和寫入。app

Buffer 類結構圖

Buffer 的類結構圖以下:jvm

從圖中發現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從新從該位置讀取數據。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;
}

2. 位置(position)

緩衝區中讀取或寫入的下一個位置。這個位置從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;
}

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)

緩衝區能夠保存元素的最大數量。該值在建立緩存區時指定,一旦建立完成後就不能修改該值。.net

//獲取緩衝區的容量
public final int capacity() {
    return capacity;
}

filp 方法

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
  1. 將limit設置成當前position的座標
  2. 將position設置爲0
  3. 取消標記

rewind 方法

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

從源碼中發現,rewind修改了position和mark,而沒有修改limit。

  1. 將position設置爲0
  2. 取消mark標記

clear 方法

public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
  1. 將position座標設置爲0
  2. limit設置爲capacity
  3. 取消標記

從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 方法

  1. public byte get()
    獲取position座標元素,並將position+1;
  2. public byte get(int i)
    獲取指定索引下標的元素
  3. public ByteBuffer get(byte[] dst)
    從當前position中讀取元素填充到dst數組中,每填充一個元素position+1;
  4. public ByteBuffer get(byte[] dst, int offset, int length)
    從當前position中讀取元素到dst數組的offset下標開始填充length個元素。

put 方法

  1. public ByteBuffer put(byte x)
    寫入一個元素並position+1
  2. public ByteBuffer put(int i, byte x)
    指定的索引寫入一個元素
  3. public final ByteBuffer put(byte[] src)
    寫入一個本身數組,並position+數組長度
  4. public ByteBuffer put(byte[] src, int offset, int length)
    從一個本身數組的offset開始length個元素寫入到ByteBuffer中,並把position+length
  5. public ByteBuffer put(ByteBuffer src)
    寫入一個ByteBuffer,並position加入寫入的元素個數

視圖緩衝區

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]

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

從結果中發現直接緩衝區建立分配空間比較耗時。

對比結論

直接緩衝區比較適合讀寫操做,最好能重複使用直接緩衝區並屢次讀寫的操做。
堆緩衝區比較適合建立新的緩衝區,而且重複讀寫不會太多的應用。

建議:若是通過性能測試,發現直接緩衝區確實比堆緩衝區效率高才使用直接緩衝區,不然不建議使用直接緩衝區,由於JVM垃圾回收不會回收直接分配的物理內存,只回收虛擬機堆內存。

相關文章
相關標籤/搜索