ByteBuffer
ByteBuffer是一個(gè)抽象類,NIO編程中經(jīng)常會使用,Netty常用的ByteBuf實(shí)際上也是對其的一種封裝
-
Byte即字節(jié),一個(gè)8位的二進(jìn)制 -
Buffer即緩沖區(qū),所謂緩沖區(qū),其實(shí)就是一個(gè)臨時(shí)存儲數(shù)據(jù)的容器(可以理解為一個(gè)數(shù)組),而且一般可以重用
緩沖區(qū)
緩沖區(qū)有什么用吶?
- 減少實(shí)際的物理讀寫次數(shù)
- 緩沖區(qū)創(chuàng)建時(shí)分配固定內(nèi)存,這塊內(nèi)存區(qū)域可被重用,減少動態(tài)分配和回收內(nèi)存的次數(shù)
舉個(gè)簡單的例子 比如我們?nèi)ト】爝f(數(shù)據(jù)),快遞很多,一次只能取一個(gè),那我們就需要來回跑很多趟(實(shí)際讀寫次數(shù))
加入我們有個(gè)大筐,一次把快遞全裝回來,就省了不少事
這個(gè)大筐在這個(gè)過程就扮演一個(gè)“緩沖區(qū)”的作用,下次取快遞還能用
byte[]
Buffer類是JDK1.4引入的NIO包中定義的一個(gè)抽象類,那我們先看看1.4之前一般是如何從管道獲取數(shù)據(jù)的,大概寫法如下:
byte[] bytes = new byte[1024];
int read = clientSocket.getInputStream().read(bytes);
System.out.println("received data:" + new String(bytes, 0, read));
我們接收IO流字節(jié)數(shù)據(jù)的方式是用一個(gè)byte[]來保存,這個(gè)byte[]其實(shí)已經(jīng)起到一個(gè)緩沖區(qū)的作用,就是用起來不太方便,也不好重復(fù)利用
而NIO出版的ByteBuffer可以理解為對byte[]的一個(gè)封裝,使其更易用于臨時(shí)數(shù)據(jù)緩沖場景
ByteBuffer繼承自Buffer類,Buffer類就是對緩沖區(qū)的一種抽象,讓我們看看作為一個(gè)Buffer有哪些特性
Buffer
Buffer是一個(gè)線性的、有界、方便重用的容器
屬性
它有4個(gè)重點(diǎn)屬性,capacity,limit,position,mark,我不先介紹其含義,從實(shí)際使用角度闡述為什么需要這4個(gè)屬性
首先作為一個(gè)有界容器,那肯定是要明確標(biāo)識界限的,這樣可以知道容器到底有多大,需要開辟多少空間,所以需要有個(gè)capacity代表容器的容量
作為一個(gè)線性容器,使用者希望寫方法只要告訴容器寫的是什么即可,而不用像數(shù)組一樣需要指定index,取方法也一樣,取完某一個(gè)再次取就接著取下一個(gè),不需要指定index,所以就需要有個(gè)屬性來標(biāo)識當(dāng)前讀/寫的位置,即position,每次讀/寫結(jié)束,直接把position向后移動一位,下一次讀/寫就是下一個(gè)元素

Buffer支持讀取操作時(shí)需要知道總共有多少可讀,這個(gè)值并非capacity,因?yàn)槿萜骺赡芪礉M,同時(shí)寫操作,由于Buffer可重復(fù)利用,每次的最大可寫量也并不一定是capacity,這兩種需求都需要有個(gè)讀寫界限值,用limit標(biāo)識
有時(shí)候我們需要從某個(gè)位置讀完數(shù)據(jù)可能過一會又想從之前哪個(gè)位置重新讀取一次,但關(guān)鍵我們的Buffer是線性的,position只能增不能減,如何找到之前的位置?所以buffer提供了一個(gè)mark屬性讓使用者可以標(biāo)識之前的一個(gè)位置,并提供mark()方法讓mark值等于position,讀/寫一段時(shí)間postion值變大了,可以調(diào)用reset()方法,讓postion回到mark的值,這就可以重新從mark點(diǎn)位讀取了

方法
上文已介紹兩個(gè)針對mark屬性的方法:mark() 和 rest(),除此之外還有幾個(gè)方便的方法:
clear() :清空的意思,清空后就可以再次利用,所以說buffer很方便重用,clear方法把limit=capacity,position=0,mark=-1(置空),為了重新寫入做好準(zhǔn)備(實(shí)際上并沒有清空元素)
flip():字面意思翻轉(zhuǎn),實(shí)際實(shí)現(xiàn)是limit=position,position=0,為讀取做好準(zhǔn)備,一般是一個(gè)Buffer寫完數(shù)據(jù)后轉(zhuǎn)換為讀模式時(shí)使用,所以名字叫翻轉(zhuǎn)還是很貼切,翻轉(zhuǎn)時(shí)limit=position記錄了當(dāng)前寫到的最大位置,也是可讀的最大位置,而position=0從頭開始讀
rewind:倒帶,主要為了重新讀,實(shí)現(xiàn)是position=0
HeapByteBuffer
講完Buffer再次回到抽象類ByteBuffer,顧名思義,就是一個(gè)存字節(jié)的Buffer,他的一個(gè)重要屬性:hb

就是被ByteBuffer封裝的byte數(shù)組,而后面的注釋說只有heap buffers使用這個(gè)屬性,實(shí)現(xiàn)代表就是HeapByteBuffer,Heap代表了這種Buffer的實(shí)際存儲地址是在堆內(nèi)存中,就是hb屬性指向的堆內(nèi)存空間
那還有什么存儲方式吶,就要介紹ByteBuffer的另一個(gè)實(shí)現(xiàn)DirectByteBuffer
DirectByteBuffer
DirectByteBuffer作為ByteBuffer自然是一個(gè)臨時(shí)存儲Byte的容器,但它的數(shù)據(jù)不存儲在堆里,那么還能怎么存?存磁盤嗎?
存磁盤顯然是不可能,那慢死了,實(shí)際上DirectByteBuffer內(nèi)的字節(jié)還是要存儲在物理內(nèi)存中,只不過并不屬于java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,而是直接內(nèi)存,也叫堆外內(nèi)存

上圖中HeapByteBuffer也是我們最常用的方式指向?qū)χ袃?nèi)存byte[]的地址,當(dāng)讀取IO數(shù)據(jù)時(shí)先把數(shù)據(jù)拷貝到直接內(nèi)存,再拷貝到j(luò)vm內(nèi)存中,兩次拷貝
而DirectByteBuffer直接指向直接內(nèi)存,省去了一步拷貝工作,這種技術(shù)也叫零拷貝,讀取數(shù)據(jù)更快
對比
那問題就來了,既然直接內(nèi)存IO速度都很快,為啥我們常用的確實(shí)HeapByteBuffer?
相比于堆內(nèi)存,直接內(nèi)存的分配時(shí)間較長,因?yàn)镴VM內(nèi)存是物理內(nèi)存提前分配好的,屬于虛擬機(jī)自己的內(nèi)存分配肯定很快,而堆外內(nèi)存需要重新向物理內(nèi)存索要額外空間,肯定需要更長時(shí)間
還有一個(gè)重要原因:堆外內(nèi)存不受GC管控,容易造成內(nèi)存溢出(可以調(diào)用system.gc手動GC)
ByteBuf
netty中封裝了一個(gè)ByteBuf,就使用到了DirectByteBuffer來創(chuàng)建直接內(nèi)存,實(shí)現(xiàn)零拷貝,那么上面介紹了使用直接內(nèi)存的缺點(diǎn)netty是如何攻破的吶
內(nèi)存池設(shè)計(jì)
針對直接內(nèi)存分配時(shí)間長的問題,netty使用內(nèi)存池設(shè)計(jì),為了盡量重用緩沖區(qū)減少分配時(shí)間,Netty提供了基于ByteBuf內(nèi)存池的緩沖區(qū)重用機(jī)制。需要的時(shí)候直接從池子里獲取ByteBuf使用即可,使用完畢之后就重新放回到池子里去
至于堆外內(nèi)存不收GC管控問題,畢竟只是一個(gè)代碼難寫的問題,只要考慮到了手動回收即可
當(dāng)然除零拷貝之外,ByteBuf還做了一些改進(jìn),使這個(gè)字節(jié)緩沖區(qū)更適用于網(wǎng)絡(luò)IO場景
讀寫索引分離
相比如Buffer設(shè)計(jì)的position同時(shí)標(biāo)志讀寫位置這種用起來很蹩腳的方式,ByteBuf提供了兩個(gè)索引:readerIndex 和 writerIndex

通過readerindex和writerIndex和capacity,將buffer分成三個(gè)區(qū)域
- 已經(jīng)讀取的區(qū)域:[0,readerindex)
- 可讀取的區(qū)域:[readerindex,writerIndex)
- 可寫的區(qū)域: [writerIndex,capacity)
動態(tài)擴(kuò)容
使用ByteBuf時(shí)會初始化一個(gè)容量,寫入時(shí),如果剩余容量不足以存放待存數(shù)據(jù),會觸發(fā)動態(tài)擴(kuò)容
由于ByteBuf使用的是直接內(nèi)存,每次都需要向操作系統(tǒng)申請一塊更大的內(nèi)存,消耗較大(雖然有池化技術(shù),但頻繁擴(kuò)容依然很浪費(fèi)),所以ByteBuf動態(tài)擴(kuò)容時(shí),并不是缺多少補(bǔ)多少,而是按一定策略進(jìn)行擴(kuò)容(通俗點(diǎn)說就是要的時(shí)候盡量多給點(diǎn),省著老要),以下是ByteBuf動態(tài)擴(kuò)容策略依據(jù)的幾個(gè)重要的參數(shù):
- minNewCapacity:為能保證本次寫入,所需的最小容量,即擴(kuò)容大于這個(gè)容量,才能裝下待寫入數(shù)據(jù)
- threshold:Bytebuf內(nèi)部設(shè)定一個(gè)分水嶺,容量在這個(gè)閾值之下和之上使用擴(kuò)容策略不同,固定4m
- maxCapacity:Netty最大能接受的容量大小,也就是可擴(kuò)容的上限,默認(rèn)為int的最大值
所以ByteBuf的擴(kuò)容策略主要分為兩種,當(dāng)minNewCapacity<threshold和minNewCapacity>threshold時(shí)使用的擴(kuò)容策略不同
1.如果minNewCapacity<threshold,翻倍擴(kuò)容,以64(字節(jié))作為基本數(shù)值,循環(huán)翻倍計(jì)算:64 -->128 --> 256,直到計(jì)算的結(jié)果大于或等于需要的容量值,則以這個(gè)結(jié)果作為實(shí)際擴(kuò)充后的新容量

2.如果minNewCapacity>threshold,用每次步進(jìn)4MB的方式進(jìn)行內(nèi)存擴(kuò)張(每次 +4MB 步進(jìn)), 如果超過maxCapacity直接使用maxCapacity作為實(shí)際容量值

3.如果minNewCapacity==threshold,直接擴(kuò)容到threshold(這種比較極端)

ByteBuf之所以使用threshold分水嶺來區(qū)分?jǐn)U容策略,主要考慮的是:
- 容量基數(shù)小時(shí)快速增長,避免頻繁擴(kuò)容(因?yàn)榛鶖?shù)小,翻倍也翻不了太多)
- 容量基數(shù)大的時(shí)候如果繼續(xù)翻倍,一次申請的容量就會很大,很大概率造成浪費(fèi),所以每次固定容量步進(jìn)增長
而threshold=4m就是 Netty 官方結(jié)合網(wǎng)絡(luò)通訊場景精算出來的最優(yōu)分界點(diǎn)。