NIO 看破也說破(五): 搞,今天就搞,搞懂Buffer

前言

 

Java NIO 中的三件法寶:ChannelSelectorBuffer 。前面幾節中,我們花了很大篇幅講過 Selector ,咱們今天只搞 Buffer 。希望能通過本文搞明白 Buffer 的基本用法和原理。

掌握重點:

  1. 兩個重要指針不停變換

  2. 一塊 Buffer 可讀可寫

  3. 基本操作的 api 用法

  4. 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 數組來進行批量讀寫,或者使用 BufferedReaderBufferedInputStream 來做讀寫緩衝區。

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個成員變量:

  1. mark:初始值爲 -1,用於備份當前的 position ;

  2. position:初始值爲 0,position 表示當前可以寫入或讀取數據的位置,當寫入或讀取一個數據後,position 向前移動到下一個位置;

  3. limit:寫模式下,limit 表示最多能往 Buffer 裏寫多少數據,等於 capacity 值;讀模式下,limit 表示最多可以讀取多少數據。

  4. 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 ,這裏就不再贅述。

 

結論

 

  1. Buffer 中有兩個重要指針 position 和 limit 不停變換位置

  2. 一塊 Buffer 可讀可寫,內部是一個 capacity 大小的數組

  3. 基本操作的 api 用法,put 、get、flip、mark、clear

  4. flip 方法改變了指針 position 和 limit 的位置

  5. 可以在 JVM 堆外分配直接內存

 

今天就搞到的這裏,劃的重點需要牢記,Buffer 的操作不注意順序會出現各種問題。

 

系列

 

NIO 看破也說破(一)—— Linux/IO 基礎

NIO 看破也說破(二)—— Java 中的兩種 BIO

NIO 看破也說破(三)—— 不同的 IO 模型

NIO 看破也說破(四)—— Java 的 NIO

NIO 看破也說破(五): 搞,今天就搞,搞懂Buffer

 

關注我

 

如果您在微信閱讀,請您點擊鏈接 關注我 ,如果您在 PC 上閱讀請掃碼關注我,歡迎與我交流隨時指出錯誤

小眼睛聊技術

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章