淺談ByteBuffer與ByteBuf

I/O

作為開(kāi)發(fā)者,I/O是一定會(huì)遇到的。以常見(jiàn)的文件操作為例,原生的java代碼如下:

// 基本字節(jié)流一次讀寫(xiě)一個(gè)字節(jié)數(shù)組
public static void method2(String srcString, String destString)
        throws IOException {
    FileInputStream fis = new FileInputStream(srcString);
    FileOutputStream fos = new FileOutputStream(destString);

    byte[] bys = new byte[1024];
    int len = 0;
    while ((len = fis.read(bys)) != -1) {
        fos.write(bys, 0, len);
    }

    fos.close();
    fis.close();
}

用FileInputStream能完成基本的功能,但是會(huì)有一個(gè)問(wèn)題就是,性能上還有優(yōu)化的空間。
問(wèn)題出在哪里?FileInputStream.read和FileOutputStream.write這兩個(gè)方法,每次執(zhí)行都會(huì)進(jìn)行I/O操作,由于I/O操作是比較好資源的,頻繁的操作必然導(dǎo)致性能上的問(wèn)題。

為什么使用Buffer

上面我們講到FileInputStream.read和FileOutputStream.write會(huì)頻繁地進(jìn)行I/O操作。為了解決這個(gè)阻塞的問(wèn)題,引入了Buffer這個(gè)概念。
Buffer做了什么事情呢?有了Buffer以后,每次的讀取,java會(huì)讀取更多的數(shù)據(jù)到緩沖區(qū)里,下次再調(diào)用read方法時(shí),如果緩沖區(qū)里的數(shù)據(jù)夠了,就直接返回?cái)?shù)據(jù),不用再執(zhí)行I/O操作。寫(xiě)入也是如此,通過(guò)這種方式,化零為整,減低了I/O操作的頻率,提升效率。

buffer 的主要目的進(jìn)行流量整形,把突發(fā)的大數(shù)量較小規(guī)模的 I/O 整理成平穩(wěn)的小數(shù)量較大規(guī)模的 I/O,以減少響應(yīng)次數(shù)(比如從網(wǎng)上下電影,你不能下一點(diǎn)點(diǎn)數(shù)據(jù)就寫(xiě)一下硬盤(pán),而是積攢一定量的數(shù)據(jù)以后一整塊一起寫(xiě),不然硬盤(pán)都要被你玩壞了)。

// 高效字節(jié)流一次讀寫(xiě)一個(gè)字節(jié)數(shù)組:
public static void method4(String srcString, String destString)
        throws IOException {
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
            srcString));
    //為什么不傳遞一個(gè)具體的文件或者文件路徑,而是傳遞一個(gè)OutputStream對(duì)象?
    //因?yàn)樽止?jié)緩沖區(qū)流僅僅提供緩沖區(qū),為高效而設(shè)計(jì)的。真正的讀寫(xiě)操作還是基本的流對(duì)象實(shí)現(xiàn)。

    //構(gòu)造方法可以指定緩沖區(qū)的大小,但是我們一般不用,默認(rèn)緩沖區(qū)大小就夠了
    BufferedOutputStream bos = new BufferedOutputStream(
            new FileOutputStream(destString));

    byte[] bys = new byte[1024];
    int len = 0;
    while ((len = bis.read(bys)) != -1) {
        bos.write(bys, 0, len);
    }

    bos.close();
    bis.close();
}

ByteBuffer

上面介紹了BufferedOutputStream,在網(wǎng)絡(luò)I/O方面,用的最多的就是ByteBuffer了。ByteBuffer的使用方式如下:

ByteBuffer byteBuffer = ByteBuffer.allocate(88);
String value = "netty";
byteBuffer.put(value.getBytes());
byteBuffer.flip();
byte[] valueArray = new byte[byteBuffer.remaining()];
byteBuffer.get(valueArray);
String decodeVaule = new String(valueArray);

但是JDK自帶的ByteBuffer并不足夠完美,它有以下缺陷:

  • ByteBuffer長(zhǎng)度固定,一旦分配完成,它的容量不能動(dòng)態(tài)擴(kuò)展和收縮,當(dāng)需要編碼的POJO對(duì)象大于ByteBuffer的容量時(shí),會(huì)發(fā)生索引越界異常;
  • ByteBuffer只有一個(gè)標(biāo)識(shí)位控的指針position,讀寫(xiě)的時(shí)候需要手工調(diào)用flip()和rewind()等,使用者必須小心謹(jǐn)慎地處理這些API,否則很容易導(dǎo)致程序處理失??;
  • ByteBuffer的API功能有限,一些高級(jí)和實(shí)用的特性它不支持,需要使用者自己編程實(shí)現(xiàn)。

ByteBuf

大名鼎鼎的通信框架netty為了解決ByteBuffer的缺陷,重寫(xiě)了一個(gè)新的數(shù)據(jù)接口ByteBuf。
與ByteBuffer相比,ByteBuf提供了兩個(gè)指針,分別記錄讀和寫(xiě)的操作位置。
初始分配的ByteBuf:


初始分配的ByteBuf

寫(xiě)入N個(gè)字節(jié)之后的ByteBuf:


寫(xiě)入N個(gè)字節(jié)之后的ByteBuf

讀取M(<N)個(gè)字節(jié)之后的ByteBuf:
讀取M(<N)個(gè)字節(jié)之后的ByteBuf

調(diào)用discardReadBytes操作之后的ByteBuf:
調(diào)用discardReadBytes操作之后的ByteBuf

調(diào)用clear操作之后的ByteBuf:


調(diào)用clear操作之后的ByteBuf

字節(jié)緩沖區(qū)

netty為了進(jìn)一步優(yōu)化提升性能,支持了堆外緩沖區(qū)。

屬性 Heap buffer Direct Buffer
位置 堆內(nèi) 堆外
內(nèi)存分配速度
內(nèi)粗能回收速度
Socket的I/O讀寫(xiě) 需要額外的內(nèi)存復(fù)制 不需要額外的內(nèi)存復(fù)制

netty官方有一句描述了使用直接緩沖區(qū)的風(fēng)險(xiǎn)。

allocating many short-lived direct NIO buffers often causes an OutOfMemoryError.

為了更高效地使用堆外緩沖區(qū),netty通過(guò)內(nèi)存池和引用計(jì)數(shù)很好地繞開(kāi)了Direct Buffer的劣勢(shì),發(fā)揚(yáng)了它的優(yōu)勢(shì)。

關(guān)于zero copy

Netty的零拷貝體現(xiàn)在三個(gè)方面:

  • Direct Buffers
  • Composite Buffers
  • FileChannel.transferTo

參考資料

Cache 和 Buffer 都是緩存,主要區(qū)別是什么?
IO、NIO、Netty
Using as a generic library
Netty中的零拷貝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容