NIO-Buffer



NIO-Buffer

目錄

NIO-概覽
NIO-Buffer
NIO-Channel
NIO-Channel接口分析java

前言

原本是想學習Netty的,可是Netty是一個NIO框架,所以在學習netty以前,仍是先梳理一下NIO的知識。經過剖析源碼理解NIO的設計原理。編程

本系列文章針對的是JDK1.8.0.161的源碼。數組

什麼是Buffer

Buffer是NIO用於存放特定基元類型數據的容器。緩衝區是特定基元類型的元素的線性有限序列。經過容量(capacity)、限制(limit)和位置(position)三個屬性控制數據的寫入大小和可讀大小。緩存

Buffer.png

  1. 容量微信

    容量是它包含的元素數。 緩衝區在建立初始化容量以後容量就不會再更改。網絡

  2. 偏移量框架

    偏移量是要讀取或寫入的下一個元素的索引。 偏移量不會大於其容量大小。性能

  3. 限制大小學習

    緩衝區的限制大小是最大可讀或可寫的索引位置,緩衝區限制大小不會大於其容量。

  4. 標誌

    能夠經過mark()方法打一個標誌,經過reset()能夠將偏移位置恢復到標誌位置。

Buffer能夠在寫模式和讀模式進行切換。在寫模式寫入數據後切換到讀模式能夠確保讀取的數據不會超過寫入數據的容量大小。

緩衝區類型

除了bool類型之外每一個基元類型都會有緩衝區

類型 緩衝區
byte ByteBuffer
char CharBuffer
double DoubleBuffer
float FloatBuffer
int IntBuffer
long LongBuffer
short ShortBuffer

緩衝區存儲類型

緩衝區分爲HeapBufferDirectBuffer

HeapBuffer是堆緩衝區,分配在堆上,有java虛擬機負責垃圾回收。
DirectBuffer是Java Native Interface(JNI,Java本地接口)在虛擬機外的內存中分配了一塊緩衝區。這塊緩衝區不直接有GC回收,在DirectBuffer包裝類對象被回收時,會經過Java Reference機制來釋放該內存塊。即當引用了DirectBuffer對象被GC回收後,操做系統纔會釋放DirectBuffer空間。

DirectByteBuffer是經過虛引用(Phantom Reference)來實現堆外內存的釋放的。虛引用主要被用來跟蹤對象被垃圾回收的狀態,經過查看引用隊列中是否包含對象所對應的虛引用來判斷它是否即將被垃圾回收,從而採起行動。它並不被期待用來取得目標對象的引用,而目標對象被回收前,它的引用會被放入一個 ReferenceQueue對象中,從而達到跟蹤對象垃圾回收的做用。

當使用HeapBuffer時,若是咱們要向硬盤讀取數據時,硬盤的數據會先複製到操做系統內核空間,操做系統內核再複製到堆緩衝區中,最後咱們在從堆緩衝區讀取字節數據。
當使用DirectBuffer時,若是咱們要向硬盤讀取數據時,硬盤的數據會先複製到操做系統內核空間,咱們直接從內核空間讀取字節數據。

因爲JVM堆中分配和釋放內存比系統分配和釋放內存更高效,所以DirectBuffer儘量重用來提升性能。

- HeapBuffer DirectBuffer
分配位置 堆內 堆外(操做系統內核)
誰來釋放 GC 當GC回收完對象時,操做系統會釋放堆外內存
建立和釋放性能
讀寫性能 JVM多一次內存複製,性能低 直接讀取操做系統內核,性能高

字節存放順序

大端模式(Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端)
小端模式:Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。

在NIO中以BufferOrder來區分大端仍是小端。

public final class ByteOrder {
    private String name;
    public static final ByteOrder BIG_ENDIAN = new ByteOrder("BIG_ENDIAN");
    public static final ByteOrder LITTLE_ENDIAN = new ByteOrder("LITTLE_ENDIAN");

    private ByteOrder(String var1) {
        this.name = var1;
    }

    public static ByteOrder nativeOrder() {
        return Bits.byteOrder();
    }

    public String toString() {
        return this.name;
    }
}

Buffer使用

接下來以ByteHeapBuffer爲例,講解如何使用Buffer。

Buffer

方法 說明
position 移動偏移量指針
limit 移動限制大小指針
mark 打標記,寄了當前偏移量的位置。可以使用reset恢復到標記位置
reset 恢復到標記位置
clear 初始化指針,清理全部數據,轉換爲寫模式(實際只是偏移指針,數據還在)
flip 轉換爲讀取模式
rewind 重置偏移量指針到初始狀態,能夠從新寫入或從新讀取
remaining 可讀或可寫容量
hasRemaining 是否可讀或可寫
hasArray 是否有數組緩存,若爲堆緩衝區,則會有數據緩存,若爲直接緩衝區,則沒有。
offset 當前數組偏移量,當把當前數組切片時,無需複製內存,直接指向偏移量。

ByteBuffer

爲了更清晰的說明緩衝區的功能,接下來以ByteBuffer舉例。

各數據類型的緩衝區除了類型不同,功能上基本是大同小異。

方法 說明
allocate 申請堆緩衝區
allocateDirect 申請直接緩衝區
wrap 將字節數組包在緩衝區中,能夠理解爲將字節數組轉換爲字節堆緩衝區
slice 緩衝區切片,當前偏移量到當前限制大小的內存生成一個緩衝區,無需複製內存,直接指向偏移量。
duplicate 共享一份緩衝區,緩衝區內容修改會互相影響,讀取互不影響
asReadOnlyBuffer 拷貝一份只讀的緩衝區。
ix 根據實際的offset偏移,對於外部來講是透明的,好比緩衝區切片以後,生成新的緩衝區實際是同一片內存,只是新的緩衝區存在offset偏移量,對切片後的緩衝區讀寫都會作偏移操做。
compact 初始化指針,清理已讀取數據,轉換爲寫模式(實際只是偏移指針position,數據還在)
getXXX 讀取數據
putXXX 寫入數據
asXXXBuffer 轉換爲指定類型的緩衝區,字節緩衝區能夠轉換爲其餘基元類型的緩衝區,其餘基元類型緩衝區不能反過來轉換

經過asXXXBuffer轉換能夠轉換爲對應的大端或小端數據但是讀取方式,好比轉換爲double類型有ByteBufferAsDoubleBufferBByteBufferAsDoubleBufferL分別對應大端和小段。
對於HeapByteBufferDirectByteBuffer接口都是同樣的,只是實現不同,一個是操做堆內存,一個是操做直接內存。

  1. 申請緩衝區
    • allocate
    ByteBuffer byteBuffer = ByteBuffer.allocate(8);
    • allocateDirect
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);

    20191206172704.png

    DirectByteBuffer(int cap) {                   
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();  //是否頁對齊
        int ps = Bits.pageSize();    //獲取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)) {
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        // 構建Cleaner對象用於跟蹤DirectByteBuffer對象的垃圾回收,以實現當DirectByteBuffer被垃圾回收時,堆外內存也會被釋放
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
  2. 寫入數據

    byte[] data = new byte[] {'H','E','L','L','O'};
    byteBuffer.put(data);

    20191206161256.png

    • 堆緩衝區寫入數據data

      public ByteBuffer put(byte[] src, int offset, int length) {
      //校驗傳入的參數是否合法
      checkBounds(offset, length, src.length);
      //在寫入數據時首先會判斷可寫容量,大於容量則會拋出`BufferOverflowException`
      if (length > remaining())
          throw new BufferOverflowException();
      //將數據寫入到指定的位置
      System.arraycopy(src, offset, hb, ix(position()), length);
      //更新偏移量
      position(position() + length);
      return this;
      }
    • 直接緩衝區寫入數據

      public ByteBuffer put(byte[] src, int offset, int length) {
          //當寫入長度大於JNI_COPY_FROM_ARRAY_THRESHOLD(6)時寫入
          if ((length << $LG_BYTES_PER_VALUE$) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
              checkBounds(offset, length, src.length);
              int pos = position();
              int lim = limit();
              assert (pos <= lim);
              int rem = (pos <= lim ? lim - pos : 0);
              if (length > rem)
                  throw new BufferOverflowException();
      
              Bits.copyFromArray(src, arrayBaseOffset, offset << $LG_BYTES_PER_VALUE$,
                                  ix(pos), length << $LG_BYTES_PER_VALUE$);
              position(pos + length);
          } else {
              //當長度小於6時,逐字節寫入
              super.put(src, offset, length);
          }
      }
      //super.put(src, offset, length);
      public ByteBuffer put(byte[] var1, int var2, int var3) {
          checkBounds(var2, var3, var1.length);
          if (var3 > this.remaining()) {
              throw new BufferOverflowException();
          } else {
              int var4 = var2 + var3;
      
              for(int var5 = var2; var5 < var4; ++var5) {
                  this.put(var1[var5]);
              }
      
              return this;
          }
      }

      這裏以6爲界限的目的是什麼?會有多少性能差別,哪位同窗清楚的話麻煩告知一下。

  3. 轉換爲讀模式

    byteBuffer.flip();

    20191206153638.png

    public final Buffer flip() {
        //當前可讀位置指向,寫入的位置
         this.limit = this.position;
         //讀取開始位置置爲0
         this.position = 0;
         this.mark = -1;
         return this;
     }
  4. 讀取數據

    byte[] data1 = new byte[3];
     byteBuffer.get(data1);

    20191206153935.png

    • 堆緩衝區讀取數據data
    public ByteBuffer get(byte[] dst, int offset, int length) {
        //檢查傳入參數
        checkBounds(offset, length, dst.length);
        //超過可讀大小拋出BufferUnderflowException異常
        if (length > remaining())
            throw new BufferUnderflowException();
        //根據實際this.offset偏移後的位置讀取數據
        System.arraycopy(hb, ix(position()), dst, offset, length);
        position(position() + length);
        return this;
    }
    • 直接緩衝區讀取數據data
    public ByteBuffer get(byte[] dst, int offset, int length) {
        //當讀取長度大於6時複製,小於6時逐字節複製
    
        if ((length << $LG_BYTES_PER_VALUE$) > Bits.JNI_COPY_TO_ARRAY_THRESHOLD) {
            checkBounds(offset, length, dst.length);
            int pos = position();
            int lim = limit();
            assert (pos <= lim);
            int rem = (pos <= lim ? lim - pos : 0);
            //超過可讀大小拋出BufferUnderflowException異常
            if (length > rem)
                throw new BufferUnderflowException();
            Bits.copyToArray(ix(pos), dst, arrayBaseOffset,
                                offset << $LG_BYTES_PER_VALUE$,
                                length << $LG_BYTES_PER_VALUE$);
            position(pos + length);
        } else {
            super.get(dst, offset, length);
        }
        return this;
    }
  5. 緩衝區切片

    ByteBuffer sliceByteBuffer = byteBuffer.slice();

    20191206154630.png

    切片了以後換建立一個新的緩衝區,可是實際的數據內存指向的是同一塊內存。

  6. 初始化指針,清理已讀取數據

    data.compact();

    此時將data初始化,會將未讀取的2個字節複製到數組頭部,同時轉換爲寫模式。

    20191206160222.png

    public ByteBuffer compact() {
        //複製未讀取的數據到初始位置
        System.arraycopy(this.hb, this.ix(this.position()), this.hb, this.ix(0), this.remaining());
        //設置當前偏移量爲未讀取的長度即5-3=2
        this.position(this.remaining());
        //設置限制大小爲容量大小
        this.limit(this.capacity());
        //設置標記爲-1
        this.discardMark();
        return this;
    }
  7. 初始化指針,清理全部數據

    data.clear();

    20191206160941.png

    完整代碼

    public static void main(String[] args) {
    
         byte[] data = new byte[] {'H','E','L','L','O'};
         System.out.println(new String(data));
         ByteBuffer byteBuffer = ByteBuffer.allocate(8);
         byteBuffer.put(data);
         byteBuffer.flip();
         byte[] data1 = new byte[3];
         byteBuffer.get(data1);
         System.out.println(new String(data1));
         ByteBuffer sliceByteBuffer = byteBuffer.slice();
         byte[] data2 = new byte[2];
         sliceByteBuffer.get(data2);
         System.out.println(new String(data2));
         byteBuffer.compact();
         byteBuffer.clear();
     }

總結

NIO經過引入緩衝區的概念使得對字節操做比傳統字節操做會方便一些,可是讀寫模式須要來回轉換會讓人有點頭暈。

相關文獻

  1. 解鎖網絡編程之NIO的前世此生
  2. 史上最強Java NIO入門:擔憂從入門到放棄的,請讀這篇!
  3. Java NIO系列教程
  4. 深刻理解DirectBuffer
  5. 《Java源碼解析》NIO中的heap Buffer和direct Buffer區別
  6. Java Reference詳解
  7. Direct Buffer vs. Heap Buffer
  8. JAVA之Buffer介紹
  9. 詳解大端模式和小端模式
  10. 堆外內存 之 DirectByteBuffer 詳解

20191127212134.png
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:http://www.javashuo.com/article/p-vknxonuq-gd.html 做者:傑哥很忙 本文使用「CC BY 4.0」創做共享協議。歡迎轉載,請在明顯位置給出出處及連接。

相關文章
相關標籤/搜索