NIo開發(fā)利器ByteBuffer
有道無術(shù),術(shù)尚可求也!有術(shù)無道,止于術(shù)!
想要使用NIO開發(fā)Socket分服務(wù)端和客戶端,必須掌握的一個知識點就是ByteBuffer的使用,他是NIO再數(shù)據(jù)傳輸中的利器!相比于BIO傳輸過程中的字節(jié)流,ByteBuffer更能體現(xiàn)出服務(wù)端/客戶端對于數(shù)據(jù)的操作效率,ByteBuffer內(nèi)存維護一個指針,使得傳輸?shù)臄?shù)據(jù)真正的能夠達(dá)到重復(fù)使用,重復(fù)讀寫的能力!
主要API和屬性

他是對于Buffer的一個默認(rèn)實現(xiàn),具體主要的屬性和方法我們需要看Buffer類:
主要屬性
//指針標(biāo)記
private int mark = -1;
//指針的當(dāng)前位置
private int position = 0;
//翻轉(zhuǎn)后界限
private int limit;
//最大容量
private int capacity;
//當(dāng)為堆外內(nèi)存的時候,內(nèi)存的地址
long address;
主要方法
//返回當(dāng)前緩沖區(qū)的最大容量
public final int capacity() {return capacity;}
//返回當(dāng)前的指針位置
public final int position() {return position;}
//返回當(dāng)前的讀寫界限
public final int limit() {return limit;}
//標(biāo)記當(dāng)前指針位置
public final Buffer mark() {
mark = position;
return this;
}
//恢復(fù)當(dāng)前指針位置
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
//清空緩沖區(qū),注意這里并不會清空數(shù)據(jù),只是將各項指標(biāo)初始化,后續(xù)再寫入數(shù)據(jù)就直接覆蓋
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
//切換讀寫模式
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
//重新從頭進行讀寫,初始化指針和標(biāo)記位置
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
//剩余可讀可寫的數(shù)量
public final int remaining() {return limit - position;}
//當(dāng)前是否可讀/可寫
public final boolean hasRemaining() {return position < limit;}
//是不是只讀的
public abstract boolean isReadOnly();
//是不是支持?jǐn)?shù)組訪問
public abstract boolean hasArray();
//獲取當(dāng)前緩存的字節(jié)數(shù)組(當(dāng)hasArray返回為true的時候)
public abstract Object array();
//是不是堆外緩沖區(qū)也就是直接緩沖區(qū)
public abstract boolean isDirect();
//取消緩沖區(qū)
final void discardMark() {mark = -1;}
堆內(nèi)緩沖
什么是堆內(nèi)緩沖區(qū)?所謂的堆內(nèi)緩沖區(qū),顧名思義就是再JVM對上分配的緩沖區(qū),一般由**byte[]**實現(xiàn),它有一個好處,就是它的內(nèi)存的分配與回收由JVM自動完成,用戶不必自己再操心內(nèi)存釋放的問題,但是缺點也很明顯,就是它再數(shù)據(jù)傳輸?shù)臅r候,需要將數(shù)據(jù)從JVM復(fù)制到本地物理內(nèi)存上,多了一次復(fù)制操作!
創(chuàng)建堆內(nèi)緩沖區(qū)
java堆內(nèi)緩沖區(qū)的默認(rèn)實現(xiàn)是 HeapByteBuffer,但是這個對象是一個 default權(quán)限的類,你是無法直接創(chuàng)建的,只能通過JDK底層暴露的api來創(chuàng)建:
//1. 分配一個最大能夠存儲128個字節(jié)的堆內(nèi)存
ByteBuffer heapRam = ByteBuffer.allocate(128);
//2. 或者直接初始化數(shù)據(jù)創(chuàng)建
ByteBuffer wrapBuffer = ByteBuffer.wrap("歡迎關(guān)注公眾號:【源碼學(xué)徒】 學(xué)習(xí)更多源碼知識!".getBytes());
堆內(nèi)緩沖區(qū)API源碼解析
構(gòu)造方法
以上兩種方案創(chuàng)建的都是一個堆內(nèi)緩沖區(qū),他們創(chuàng)建的邏輯大致相同,我們以 ByteBuffer.allocate為例進行分析:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException();
}
//創(chuàng)建一個堆內(nèi)緩沖區(qū)
return new HeapByteBuffer(capacity, capacity);
}
我們可以看到,通過 ByteBuffer.allocate創(chuàng)建的緩沖區(qū)是一個 HeapByteBuffer,他是堆內(nèi)緩沖區(qū)!我們繼續(xù)往下分析:
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0);
}
注意此時,cap和lim都是我們傳遞的大小,內(nèi)部還創(chuàng)建了一個cap大小的字節(jié)數(shù)組傳遞下去!他就是堆內(nèi)最終存儲數(shù)據(jù)的數(shù)組!
// mark = -1 pos = 0 lim = 128 cap = 128 hb = 字節(jié)數(shù)組對象 offset = 0
ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {
super(mark, pos, lim, cap);
//前面創(chuàng)建的字節(jié)數(shù)組對象
this.hb = hb;
//保存一個偏移量 默認(rèn)為0
this.offset = offset;
}
然后再調(diào)用父類的構(gòu)造參數(shù):
Buffer(int mark, int pos, int lim, int cap) {
if (cap < 0){
throw new IllegalArgumentException("Negative capacity: " + cap);
}
//保存最大容量
this.capacity = cap;
//保存limit
limit(lim);
//保存pos指針位置
position(pos);
//........ 忽略無必要代碼.........
}
此時我們的一個堆內(nèi)緩沖區(qū)就創(chuàng)建完成了,它的內(nèi)部結(jié)構(gòu)如下:

put方法
現(xiàn)在我們的容器創(chuàng)建好了,我們就需要往里面懟數(shù)據(jù)了呀,我們需要往里面寫入一段字節(jié)數(shù)組:
heapRam.put("A".getBytes());
我們調(diào)用put方法往ByteBuffer里面寫入一段數(shù)據(jù)會發(fā)生什么呢?
public ByteBuffer put(byte[] src, int offset, int length) {
//先判斷當(dāng)前數(shù)據(jù)的長度是否超過可寫長度了
// remaining() = limit - position = 128 - 0
if (length > remaining()) {
throw new BufferOverflowException();
}
//hb還記得嗎,就是我們再創(chuàng)建堆內(nèi)緩沖區(qū)所創(chuàng)建的字節(jié)數(shù)組
//這里就是將我們的數(shù)據(jù)拷貝到從當(dāng)前的指針位置開始的堆內(nèi)緩存(hb字節(jié)數(shù)組)
System.arraycopy(src, offset, hb, ix(position()), length);
//將當(dāng)前的指針位置 + 數(shù)據(jù)長度,我們本次寫入的數(shù)據(jù)長度是1 那么當(dāng)前的指針?biāo)饕褪?1
position(position() + length);
return this;
}
當(dāng)調(diào)用put方法后,內(nèi)部數(shù)據(jù)結(jié)構(gòu)如下:

為了方便后續(xù)的講解我們再次寫入幾個數(shù)據(jù)的時候,邏輯和上方一樣:
heapRam.put("B".getBytes());
heapRam.put("C".getBytes());
heapRam.put("D".getBytes());
heapRam.put("E".getBytes());

get方法
我們現(xiàn)在再緩沖區(qū)里面寫入了 ABCDE五個數(shù)據(jù),此時我們?nèi)绻霃木彌_區(qū)取數(shù)據(jù),就應(yīng)該調(diào)用另外一個api:get()方法
byte b = heapRam.get();
System.out.println(new String(new byte[]{b}));
但是,很奇怪的是,我們打印了一個空,并沒有想象中的打印一個A,這是為什么呢?我們由上面的分析可以知道,每次緩沖區(qū)對于數(shù)據(jù)的操作都是基于指針來做的,我們每一次操作數(shù)據(jù),指針都會后移一位,當(dāng)我們發(fā)生一個get()請求后,指針依舊會后移,將下標(biāo)為5的數(shù)據(jù)返回同時自身自增變?yōu)?.但是下標(biāo)為5的并沒有數(shù)據(jù),只能返回一個空數(shù)據(jù),所以我們?nèi)绻霃念^讀數(shù)據(jù),就必須想辦法將指針復(fù)位,重新變?yōu)?,我們此時往里面寫數(shù)據(jù),我們稱之為寫模式,想要切換到讀模式就必須調(diào)用 **heapRam.flip();**方法來切換讀寫模式,復(fù)位讀寫指針!
heapRam.flip();
那這個api具體做了什么呢?僅僅是將讀寫指針復(fù)位嗎? 那我提出一個問題,不妨讀者讀到這里思考一下,如果僅僅是指針復(fù)位的話,我們?nèi)绾慰刂撇蛔層脩糇x超呢? **我們只寫入了5個數(shù),如何避免用戶讀第六個數(shù)據(jù)呢?**我們帶著疑問,看下 flip方法究竟做了什么:
public final Buffer flip() {
//將當(dāng)前的指針位置賦值給limit
limit = position;
//讀寫指針復(fù)位
position = 0;
mark = -1;
return this;
}
filp方法
我們可以看到,filp方法再復(fù)位讀寫指針之前,記錄了一個位置 limit,具體他是干嘛的,我么稍后再說 到現(xiàn)在為止,我們的數(shù)據(jù)結(jié)構(gòu)如下:

byte b = heapRam.get();
System.out.println(new String(new byte[]{b}));
此時我們再次調(diào)用get方法,指針后移,同時返回當(dāng)前指針位置代表的數(shù)據(jù):注意 ix方法不用管,他是計算偏移量的,這里始終是0

public byte get() {
return hb[ix(nextGetIndex())];
}
nextGetIndex:主要是判斷當(dāng)前指針是否超過了 limit的限制,同時自增指針位置
final int nextGetIndex() {
//limit的作用在這里被體現(xiàn),判斷你的讀指針是不是讀超了數(shù)據(jù)范圍
if (position >= limit){
throw new BufferUnderflowException();
}
//返回讀指針的位置,并自增1
return position++;
}
get方法主要是直接返回字節(jié)數(shù)組某個下標(biāo)的位置的字節(jié)數(shù)據(jù)!
我們多讀一些數(shù)據(jù):
byte[] bytes = new byte[4];
heapRam.get(bytes);
System.out.println(new String(bytes));
當(dāng)我們傳遞了一個字節(jié)數(shù)組去讀取的時候,它的內(nèi)部是如何做的呢?
public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
}
public ByteBuffer get(byte[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
if (length > remaining())
throw new BufferUnderflowException();
//將數(shù)據(jù)拷貝至我們傳遞的字節(jié)數(shù)組中
System.arraycopy(hb, ix(position()), dst, offset, length);
//讀指針位置+我們要讀取的長度
position(position() + length);
return this;
}
此時緩沖區(qū)的內(nèi)部數(shù)據(jù)結(jié)構(gòu)如下:

我們把數(shù)據(jù)讀完了,下面,我又想往里面寫數(shù)據(jù)了,假設(shè)直接寫是否能寫呢? put方法向下表為5的地方寫一個數(shù)據(jù),同時指針后移,似乎可行,我們分析一下put方法,具體我們前面已經(jīng)分析過了,再put方法的源碼中,有這么一段邏輯:
if (length > remaining())
throw new BufferOverflowException();
假設(shè)寫入數(shù)據(jù)的長度,大于剩余可寫長度,就會報錯,我們具體看下這個方法的邏輯:
public final int remaining() {
return limit - position;
}
我們看上圖的數(shù)據(jù)結(jié)構(gòu)數(shù)據(jù)可知,該結(jié)果為0,就必定會報錯,所以說,當(dāng)我們向再次切換成寫模式的話,就一定要初始化 pos,還是調(diào)用filp方法嗎?重新調(diào)用filp方法固然可行,但是,調(diào)用filp方法并不會初始化limit的大小,造成明明我們分配了128個字節(jié)的大小,但是可用的永遠(yuǎn)都只有5個,所以,我們?nèi)绻胱寯?shù)據(jù)重新能夠初始化,就必須讓limit = capacity,JDK也為我們提供了接口:clear
clear方法
heapRam.clear();
public final Buffer clear() {
//讀寫指針歸零
position = 0;
//limit初始化為初始狀態(tài)
limit = capacity;
//標(biāo)記初始化為初始狀態(tài)
mark = -1;
return this;
}
可以看到,clear方法將我們緩沖區(qū)中的所有指標(biāo)全部的進行初始化了,指針重新歸0,但是JDK考慮到性能影響byte數(shù)組中的數(shù)據(jù)并沒有被清除,只會被新數(shù)據(jù)覆蓋調(diào)!
由同學(xué)會問,你不是說ByteBuffer可以進行重復(fù)的讀取嗎? 這明明只能讀一遍,讀完就得初始化指針位置,你騙人!
別著急,想要進行重復(fù)的讀寫操作,我們必須還要掌握另外一組API:mark() 、reset();
我們假設(shè)我們此時處于讀模式,數(shù)據(jù)結(jié)構(gòu)如下:

mark方法
我們此時想,一會讀完數(shù)據(jù)了,還想再次回到當(dāng)前的位置進行數(shù)據(jù)的二次讀取,我們此時就應(yīng)該調(diào)用mark()方法,打個標(biāo)記,它的底層會記錄當(dāng)前指針的位置:
heapRam.mark();
public final Buffer mark() {
//記錄當(dāng)前讀指針的位置
mark = position;
return this;
}
調(diào)用mark方法之后,我們的數(shù)據(jù)結(jié)構(gòu)如下:

然后我們將數(shù)據(jù)讀完:

reset方法
我們將數(shù)據(jù)讀完之后,想再從標(biāo)記位置開始讀取的時候:
heapRam.reset();
public final Buffer reset() {
//獲取當(dāng)前標(biāo)記的位置
int m = mark;
if (m < 0)
//如果標(biāo)記位為負(fù)數(shù),就證明沒有進行標(biāo)記過直接報錯
throw new InvalidMarkException();
//然后將標(biāo)記位置賦值給當(dāng)前的指針位置
position = m;
return this;
}
當(dāng)前的數(shù)據(jù)結(jié)構(gòu)如下:

rewind方法
如此,我們就可以進行復(fù)讀了,相類似的方法還有:rewind
heapRam.rewind();
public final Buffer rewind() {
//回復(fù)讀寫指針為0
position = 0;
//廢棄標(biāo)記位置
mark = -1;
return this;
}
rewind方法是直接返回的 緩沖區(qū)的頭部,同時廢棄標(biāo)記的位置 !

堆外緩沖區(qū)
創(chuàng)建堆外緩沖區(qū)
//1. 分配一個最大能夠存儲128個字節(jié)的堆外內(nèi)存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(128);
jvm如何操作堆外內(nèi)存
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//獲取JDK底層的操作物理內(nèi)存的工具類
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe o = (Unsafe) theUnsafe.get(null);
//從物理內(nèi)存分配一塊128的內(nèi)存
long address = o.allocateMemory(128);
//獲取字節(jié)數(shù)組的一個基本偏移 數(shù)組基本偏移
long arrayBaseOffset = (long)o.arrayBaseOffset(byte[].class);
byte[] bytes = "歡迎關(guān)注公眾號:【源碼學(xué)徒】 學(xué)習(xí)更多源碼知識!".getBytes();
//向物理內(nèi)存復(fù)制一段數(shù)據(jù)
// 數(shù)據(jù)源 數(shù)據(jù)的基本偏移 目標(biāo)數(shù)據(jù)源 要復(fù)制到的內(nèi)存地址 復(fù)制數(shù)據(jù)的長度
o.copyMemory(bytes, arrayBaseOffset, null, address, bytes.length);
//從物理機將數(shù)據(jù)拷貝回JVM內(nèi)存中
byte[] copy = new byte[bytes.length];
// 數(shù)據(jù)源 物理地址 目標(biāo)數(shù)據(jù)源 數(shù)組基本偏移量 復(fù)制數(shù)據(jù)的長度
o.copyMemory(null, address, copy, arrayBaseOffset, bytes.length);
//釋放內(nèi)存
o.freeMemory(address);
System.out.println(new String(copy));
}
上述的操作是分配一個物理內(nèi)存、將一段數(shù)據(jù)寫進物理內(nèi)存、然后將數(shù)據(jù)從物理內(nèi)存讀進JVM數(shù)組、釋放物理內(nèi)存
有了基本的知識,我們一起分下下堆外內(nèi)存的源碼把!
堆外緩沖區(qū)Api源碼解析
構(gòu)造方法
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
我們可以看到,堆外緩沖區(qū)是由DirectByteBuffer來代表的!
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
//......忽略其他代碼..........
long base = 0;
try {
//從物理內(nèi)存分配一塊指定大小的內(nèi)存 并返回當(dāng)前分配內(nèi)存的地址
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
//初始化內(nèi)存
unsafe.setMemory(base, size, (byte) 0);
//判斷是否對其的頁面
if (pa && (base % ps != 0)) {
// 向上對其頁面 并保存地址
address = base + ps - (base & (ps - 1));
} else {
//保存地址
address = base;
}
//這個極其重要,是JVM管理堆外內(nèi)存的重要方法
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
我們來逐行進行分析,首先是 super(-1, 0, cap, cap);
// -1 0 128 128
MappedByteBuffer(int mark, int pos, int lim, int cap) {
//繼續(xù)調(diào)用父類
super(mark, pos, lim, cap);
this.fd = null;
}
// -1 0 128 128
ByteBuffer(int mark, int pos, int lim, int cap) {
//在往上
this(mark, pos, lim, cap, null, 0);
}
// -1 0 128 128 null 0
ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
到這里就不往上分析了,它和創(chuàng)建堆內(nèi)緩沖是一樣的,保存一些基本的變量,但是注意 這里傳遞的hb是一個null,因為它是堆外緩沖區(qū),不依賴與JVM內(nèi)部的內(nèi)存分配!

此時基本數(shù)據(jù)保存完畢,開始分配一塊堆外內(nèi)存:
base = unsafe.allocateMemory(size);
這里是調(diào)用的 native方法分配的緩沖區(qū),是C來實現(xiàn)的,unsafe是JDK內(nèi)部使用的一個操作物理內(nèi)存的工具類,一般不對外開放,如果想要使用可以通過反射的方式獲取,獲取方式上面已經(jīng)寫出來了,同學(xué)們沒事可以玩一下!
unsafe.setMemory(base, size, (byte) 0);
初始化內(nèi)存區(qū)域,將所分配的內(nèi)存里面的數(shù)據(jù)默認(rèn)設(shè)置為字節(jié)0
address = base;
保存物理內(nèi)存的地址,方面后面進行數(shù)據(jù)的讀寫
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
這個方法極其重要,主要負(fù)責(zé)改堆外內(nèi)存的釋放,他是一個虛引用,具體的講解上一篇文章 深入分析NIO的零拷貝述的很詳細(xì),看一下JVM是如何釋放一個不由JVM控制的堆外內(nèi)存的!這里就不做具體的講解了!

put方法
現(xiàn)在我們向堆外內(nèi)存寫入一段數(shù)據(jù):
byteBuffer.put("ABCDE".getBytes());
我們看下源碼是如何來操作堆外內(nèi)存的
public final ByteBuffer put(byte[] src) {
return put(src, 0, src.length);
}
public ByteBuffer put(byte[] src, int offset, int length) {
if (((long)length << 0) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
//檢查越界
checkBounds(offset, length, src.length);
//獲取當(dāng)前的讀寫指針
int pos = position();
//獲取當(dāng)前的學(xué)些界限
int lim = limit();
//判斷是否超過讀寫界限
assert (pos <= lim);
//計算剩余空間
int rem = (pos <= lim ? lim - pos : 0);
//判斷寫入數(shù)據(jù)是否大于剩余空間
if (length > rem) {
throw new BufferOverflowException();
}
//向物理內(nèi)存拷貝數(shù)據(jù)
Bits.copyFromArray(src, arrayBaseOffset, (long)offset << 0, ix(pos), (long)length << 0);
//重新計算當(dāng)前的讀寫指針
position(pos + length);
} else {
super.put(src, offset, length);
}
return this;
}
我們發(fā)現(xiàn),里面最關(guān)鍵的一段代碼是 Bits.copyFromArray(src, arrayBaseOffset, (long)offset << 0, ix(pos), (long)length << 0);
它傳遞的是:要寫入的數(shù)據(jù)的字節(jié)數(shù)組、字節(jié)基準(zhǔn)偏移、偏移量(0)、地址+讀寫指針的位置(addres + pos)、要寫入的數(shù)據(jù)的長度
static void copyFromArray(Object src, long srcBaseOffset, long srcPos, long dstAddr, long length) {
//計算 偏移量 = 字節(jié)數(shù)組基準(zhǔn)偏移量 + offset(0)
long offset = srcBaseOffset + srcPos;
//如果存在數(shù)據(jù)
while (length > 0) {
//判斷數(shù)據(jù)是否大于1MB 如果大于1MB就默認(rèn)只傳遞1MB,剩余數(shù)據(jù)交給下一次循環(huán)
long size = (length > UNSAFE_COPY_THRESHOLD) ? UNSAFE_COPY_THRESHOLD : length;
//拷貝數(shù)據(jù)
unsafe.copyMemory(src, offset, null, dstAddr, size);
//判斷本次拷貝后剩余未拷貝的數(shù)據(jù)
length -= size;
//計算偏移量 本次應(yīng)該偏移的數(shù)量
offset += size;
//計算地址 下次讀取的起始位置
dstAddr += size;
}
}
我們會發(fā)現(xiàn),里面的代碼有一部分我們極其熟悉,正是上面我演示unsafe如何使用的代碼,這里就是將數(shù)據(jù)拷貝至堆外內(nèi)存的!現(xiàn)在它的內(nèi)存結(jié)構(gòu)如下:

filp方法
byteBuffer.flip();

現(xiàn)在我們要獲取數(shù)據(jù)了就也必須調(diào)用filp方法切換讀寫模式,直接緩沖區(qū)的切換方式和堆內(nèi)內(nèi)存的切換方式 一致,不做講述,忘記的小伙伴請翻到上面看下!
get方法
byte[] bytes = new byte[5];
byteBuffer.get(bytes);
public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
}
// bytes 0 5
public ByteBuffer get(byte[] dst, int offset, int length) {
if (((long)length << 0) > Bits.JNI_COPY_TO_ARRAY_THRESHOLD) {
checkBounds(offset, length, dst.length);
//獲取當(dāng)前的讀指針 0
int pos = position();
//獲取當(dāng)前的limit
int lim = limit();
//判斷是否超過界限
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
if (length > rem) {
throw new BufferUnderflowException();
}
//關(guān)鍵方法 將物理內(nèi)存中的數(shù)據(jù)復(fù)制到JVM內(nèi)存中來
Bits.copyToArray(ix(pos), dst, arrayBaseOffset, (long)offset << 0, (long)length << 0);
//將讀寫指針切換至對應(yīng)位置 5
position(pos + length);
} else {
super.get(dst, offset, length);
}
return this;
}
可以看出,當(dāng)前的關(guān)鍵代碼是 Bits.copyToArray(ix(pos), dst, arrayBaseOffset, (long)offset << 0, (long)length << 0); 我們分析一下
static void copyToArray(long srcAddr, Object dst, long dstBaseOffset, long dstPos, long length) {
//計算當(dāng)前的偏移量
long offset = dstBaseOffset + dstPos;
while (length > 0) {
//最大拷貝長度是 1MB 高于1MB的下次循環(huán)再次拷貝
long size = (length > UNSAFE_COPY_THRESHOLD) ? UNSAFE_COPY_THRESHOLD : length;
// 將數(shù)據(jù)拷貝回指定的數(shù)組中
// 源數(shù)據(jù) 數(shù)據(jù)所在的內(nèi)存地址 目標(biāo)位置 偏移量 拷貝的長度
unsafe.copyMemory(null, srcAddr, dst, offset, size);
//計算剩余的數(shù)據(jù)長度
length -= size;
//計算下次拷貝的地址的偏移量
srcAddr += size;
//計算下次復(fù)制的偏移量
offset += size;
}
}
當(dāng)前數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)為:

堆外緩沖區(qū)比較重要的幾個點:
緩沖區(qū)的創(chuàng)建(構(gòu)造函數(shù)) 數(shù)據(jù)的存儲(put方法) 數(shù)據(jù)的獲取(get方法) 堆外內(nèi)存的釋放
都已經(jīng)介紹完畢,其他類似的API譬如 clear、mark、reset、rewind 再上面的外內(nèi)內(nèi)存的介紹中都已經(jīng)介紹完畢了,邏輯都一樣,感興趣的小伙伴可以自己追一下源碼!
對于NIO的學(xué)習(xí),這個緩沖區(qū)是必不可少的一節(jié)課!務(wù)必要搞明白呀!
才疏學(xué)淺,如果文章中理解有誤,歡迎大佬們私聊指正!歡迎關(guān)注作者的公眾號,一起進步,一起學(xué)習(xí)!
??「轉(zhuǎn)發(fā)」和「在看」,是對我最大的支持??
