NIO-Buffer
目錄
NIO-概覽
NIO-Buffer
NIO-Channel
NIO-Channel接口分析
NIO-SocketChannel源碼分析
NIO-FileChannel源碼分析
NIO-Selector源碼分析
NIO-WindowsSelectorImpl源碼分析
NIO-EPollSelectorIpml源碼分析
前言
本來是想學習Netty的,但是Netty是一個NIO框架,因此在學習netty之前,還是先梳理一下NIO的知識。通過剖析源碼理解NIO的設計原理。
本系列文章針對的是JDK1.8.0.161的源碼。
什么是Buffer
Buffer是NIO用於存放特定基元類型數據的容器。緩沖區是特定基元類型的元素的線性有限序列。通過容量(capacity)、限制(limit)和位置(position)三個屬性控制數據的寫入大小和可讀大小。
-
容量
容量是它包含的元素數。 緩沖區在創建初始化容量之后容量就不會再更改。
-
偏移量
偏移量是要讀取或寫入的下一個元素的索引。 偏移量不會大於其容量大小。
-
限制大小
緩沖區的限制大小是最大可讀或可寫的索引位置,緩沖區限制大小不會大於其容量。
-
標志
可以通過
mark()
方法打一個標志,通過reset()
可以將偏移位置恢復到標志位置。
Buffer可以在寫模式和讀模式進行切換。在寫模式寫入數據后切換到讀模式可以確保讀取的數據不會超過寫入數據的容量大小。
緩沖區類型
除了bool類型以外每個基元類型都會有緩沖區
類型 | 緩沖區 |
---|---|
byte | ByteBuffer |
char | CharBuffer |
double | DoubleBuffer |
float | FloatBuffer |
int | IntBuffer |
long | LongBuffer |
short | ShortBuffer |
緩沖區存儲類型
緩沖區分為HeapBuffer
和DirectBuffer
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類型有ByteBufferAsDoubleBufferB
和ByteBufferAsDoubleBufferL
分別對應大端和小段。
對於HeapByteBuffer
和DirectByteBuffer
接口都是一樣的,只是實現不一樣,一個是操作堆內存,一個是操作直接內存。
-
申請緩沖區
- allocate
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
- allocateDirect
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);
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; }
-
寫入數據
byte[] data = new byte[] {'H','E','L','L','O'}; byteBuffer.put(data);
-
堆緩沖區寫入數據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為界限的目的是什么?會有多少性能差異,哪位同學清楚的話麻煩告知一下。
-
-
轉換為讀模式
byteBuffer.flip();
public final Buffer flip() { //當前可讀位置指向,寫入的位置 this.limit = this.position; //讀取開始位置置為0 this.position = 0; this.mark = -1; return this; }
-
讀取數據
byte[] data1 = new byte[3]; byteBuffer.get(data1);
- 堆緩沖區讀取數據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; }
-
緩沖區切片
ByteBuffer sliceByteBuffer = byteBuffer.slice();
切片了之后換創建一個新的緩沖區,但是實際的數據內存指向的是同一塊內存。
-
初始化指針,清理已讀取數據
data.compact();
此時將data初始化,會將未讀取的2個字節復制到數組頭部,同時轉換為寫模式。
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; }
-
初始化指針,清理所有數據
data.clear();
完整代碼
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通過引入緩沖區的概念使得對字節操作比傳統字節操作會方便一些,但是讀寫模式需要來回轉換會讓人有點頭暈。
相關文獻
- 解鎖網絡編程之NIO的前世今生
- 史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!
- Java NIO系列教程
- 深入理解DirectBuffer
- 《Java源碼解析》NIO中的heap Buffer和direct Buffer區別
- Java Reference詳解
- Direct Buffer vs. Heap Buffer
- JAVA之Buffer介紹
- 詳解大端模式和小端模式
- 堆外內存 之 DirectByteBuffer 詳解
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:https://www.cnblogs.com/Jack-Blog/p/11996309.html
作者:傑哥很忙
本文使用「CC BY 4.0」創作共享協議。歡迎轉載,請在明顯位置給出出處及鏈接。