ByteBuffer
ByteBuffer
是一個抽象類狼讨,NIO編程中經(jīng)常會使用,Netty常用的ByteBuf實際上也是對其的一種封裝
-
Byte
即字節(jié)系羞,一個8位的二進制 -
Buffer
即緩沖區(qū)郭计,所謂緩沖區(qū),其實就是一個臨時存儲數(shù)據(jù)的容器(可以理解為一個數(shù)組)椒振,而且一般可以重用
緩沖區(qū)
緩沖區(qū)有什么用吶?
- 減少實際的物理讀寫次數(shù)
- 緩沖區(qū)創(chuàng)建時分配固定內(nèi)存昭伸,這塊內(nèi)存區(qū)域可被重用,減少動態(tài)分配和回收內(nèi)存的次數(shù)
舉個簡單的例子 比如我們?nèi)ト】爝f(數(shù)據(jù))澎迎,快遞很多庐杨,一次只能取一個选调,那我們就需要來回跑很多趟(實際讀寫次數(shù))
加入我們有個大筐,一次把快遞全裝回來灵份,就省了不少事
這個大筐在這個過程就扮演一個“緩沖區(qū)”的作用仁堪,下次取快遞還能用
byte[]
Buffer類是JDK1.4引入的NIO包中定義的一個抽象類,那我們先看看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ù)的方式是用一個byte[]
來保存弦聂,這個byte[]其實已經(jīng)起到一個緩沖區(qū)的作用,就是用起來不太方便氛什,也不好重復(fù)利用
而NIO出版的ByteBuffer可以理解為對byte[]的一個封裝莺葫,使其更易用于臨時數(shù)據(jù)緩沖場景
ByteBuffer繼承自Buffer類,Buffer類就是對緩沖區(qū)的一種抽象枪眉,讓我們看看作為一個Buffer有哪些特性
Buffer
Buffer
是一個線性的捺檬、有界、方便重用的容器
屬性
它有4個重點屬性瑰谜,capacity欺冀,limit树绩,position萨脑,mark,我不先介紹其含義饺饭,從實際使用角度闡述為什么需要這4個屬性
首先作為一個有界容器渤早,那肯定是要明確標(biāo)識界限的,這樣可以知道容器到底有多大瘫俊,需要開辟多少空間鹊杖,所以需要有個capacity
代表容器的容量
作為一個線性容器,使用者希望寫方法只要告訴容器寫的是什么即可扛芽,而不用像數(shù)組一樣需要指定index骂蓖,取方法也一樣,取完某一個再次取就接著取下一個川尖,不需要指定index登下,所以就需要有個屬性來標(biāo)識當(dāng)前讀/寫的位置,即position
叮喳,每次讀/寫結(jié)束被芳,直接把position向后移動一位,下一次讀/寫就是下一個元素
Buffer支持讀取操作時需要知道總共有多少可讀馍悟,這個值并非capacity畔濒,因為容器可能未滿,同時寫操作锣咒,由于Buffer可重復(fù)利用侵状,每次的最大可寫量也并不一定是capacity赞弥,這兩種需求都需要有個讀寫界限值,用limit
標(biāo)識
有時候我們需要從某個位置讀完數(shù)據(jù)可能過一會又想從之前哪個位置重新讀取一次趣兄,但關(guān)鍵我們的Buffer是線性的嗤攻,position只能增不能減,如何找到之前的位置诽俯?所以buffer提供了一個mark
屬性讓使用者可以標(biāo)識之前的一個位置妇菱,并提供mark()方法讓mark值等于position,讀/寫一段時間postion值變大了暴区,可以調(diào)用reset()方法闯团,讓postion回到mark的值,這就可以重新從mark點位讀取了
方法
上文已介紹兩個針對mark屬性的方法:mark()
和 rest()
仙粱,除此之外還有幾個方便的方法:
clear()
:清空的意思房交,清空后就可以再次利用,所以說buffer很方便重用伐割,clear方法把limit=capacity候味,position=0,mark=-1(置空)隔心,為了重新寫入做好準(zhǔn)備(實際上并沒有清空元素)
flip()
:字面意思翻轉(zhuǎn)白群,實際實現(xiàn)是limit=position,position=0硬霍,為讀取做好準(zhǔn)備帜慢,一般是一個Buffer寫完數(shù)據(jù)后轉(zhuǎn)換為讀模式時使用,所以名字叫翻轉(zhuǎn)還是很貼切唯卖,翻轉(zhuǎn)時limit=position記錄了當(dāng)前寫到的最大位置粱玲,也是可讀的最大位置,而position=0從頭開始讀
rewind
:倒帶拜轨,主要為了重新讀抽减,實現(xiàn)是position=0
HeapByteBuffer
講完Buffer再次回到抽象類ByteBuffer,顧名思義橄碾,就是一個存字節(jié)的Buffer卵沉,他的一個重要屬性:hb
就是被ByteBuffer封裝的byte數(shù)組,而后面的注釋說只有heap buffers使用這個屬性堪嫂,實現(xiàn)代表就是HeapByteBuffer
偎箫,Heap代表了這種Buffer的實際存儲地址是在堆內(nèi)存中,就是hb
屬性指向的堆內(nèi)存空間
那還有什么存儲方式吶皆串,就要介紹ByteBuffer的另一個實現(xiàn)DirectByteBuffer
DirectByteBuffer
DirectByteBuffer作為ByteBuffer自然是一個臨時存儲Byte的容器淹办,但它的數(shù)據(jù)不存儲在堆里,那么還能怎么存恶复?存磁盤嗎怜森?
存磁盤顯然是不可能速挑,那慢死了,實際上DirectByteBuffer內(nèi)的字節(jié)還是要存儲在物理內(nèi)存中副硅,只不過并不屬于java虛擬機運行時數(shù)據(jù)區(qū)的一部分姥宝,而是直接內(nèi)存,也叫堆外內(nèi)存
上圖中HeapByteBuffer也是我們最常用的方式指向?qū)χ袃?nèi)存byte[]的地址恐疲,當(dāng)讀取IO數(shù)據(jù)時先把數(shù)據(jù)拷貝到直接內(nèi)存腊满,再拷貝到j(luò)vm內(nèi)存中,兩次拷貝
而DirectByteBuffer直接指向直接內(nèi)存培己,省去了一步拷貝工作碳蛋,這種技術(shù)也叫零拷貝,讀取數(shù)據(jù)更快
對比
那問題就來了省咨,既然直接內(nèi)存IO速度都很快肃弟,為啥我們常用的確實HeapByteBuffer?
相比于堆內(nèi)存零蓉,直接內(nèi)存的分配時間較長笤受,因為JVM內(nèi)存是物理內(nèi)存提前分配好的,屬于虛擬機自己的內(nèi)存分配肯定很快敌蜂,而堆外內(nèi)存需要重新向物理內(nèi)存索要額外空間箩兽,肯定需要更長時間
還有一個重要原因:堆外內(nèi)存不受GC管控,容易造成內(nèi)存溢出(可以調(diào)用system.gc手動GC)
ByteBuf
netty中封裝了一個ByteBuf
紊册,就使用到了DirectByteBuffer來創(chuàng)建直接內(nèi)存比肄,實現(xiàn)零拷貝,那么上面介紹了使用直接內(nèi)存的缺點netty是如何攻破的吶
內(nèi)存池設(shè)計
針對直接內(nèi)存分配時間長的問題囊陡,netty使用內(nèi)存池設(shè)計
,為了盡量重用緩沖區(qū)減少分配時間掀亥,Netty提供了基于ByteBuf內(nèi)存池的緩沖區(qū)重用機制撞反。需要的時候直接從池子里獲取ByteBuf使用即可,使用完畢之后就重新放回到池子里去
至于堆外內(nèi)存不收GC管控問題搪花,畢竟只是一個代碼難寫的問題遏片,只要考慮到了手動回收即可
當(dāng)然除零拷貝之外,ByteBuf還做了一些改進撮竿,使這個字節(jié)緩沖區(qū)更適用于網(wǎng)絡(luò)IO場景
讀寫索引分離
相比如Buffer設(shè)計的position同時標(biāo)志讀寫位置這種用起來很蹩腳的方式吮便,ByteBuf提供了兩個索引:readerIndex 和 writerIndex
通過readerindex和writerIndex和capacity,將buffer分成三個區(qū)域
- 已經(jīng)讀取的區(qū)域:[0,readerindex)
- 可讀取的區(qū)域:[readerindex,writerIndex)
- 可寫的區(qū)域: [writerIndex,capacity)
動態(tài)擴容
ByteBuf內(nèi)部有這么幾個屬性
- minNewCapacity:表用戶需要寫入的目標(biāo)值大小
- threshold:閾值幢踏,為Bytebuf內(nèi)部設(shè)定容量的最大值髓需,默認4M
- maxCapacity:Netty最大能接受的容量大小,一般為int的最大值
擴容方法
1..如果目標(biāo)值等于閾值房蝉,使用閥值作為實際容量值
2.如果目標(biāo)值大于閾值僚匆,用每次步進4MB的方式進行內(nèi)存擴張((需要擴容值/4MB)X4MB), 如果超過maxCapacity直接使用maxCapacity作為實際容量值
3.如果目標(biāo)值小于閥值微渠,采用倍增的方式,以64(字節(jié))作為基本數(shù)值咧擂,每次翻倍增長64 -->128 --> 256逞盆,直到倍增后的結(jié)果大于或等于需要的容量值,倍增的結(jié)果作為實際容量值