Java性能優化之使用NIO提高性能(Buffer和Channel)

在軟件系統中,因爲IO的速度要比內存慢,所以,I/O讀寫在不少場合都會成爲系統的瓶頸。提高I/O速度,對提高系統總體性能有着很大的好處。java

在Java的標準I/O中,提供了基於流的I/O實現,即InputStream和OutputStream。這種基於流的實現以字節爲單位處理數據,而且很是容易創建各類過濾器。數組

NIO是New I/O的簡稱,具備如下特性:緩存

  1. 爲全部的原始類型提供(Buffer)緩存支持;
  2. 使用 java.nio.charset.Charset 做爲字符集編碼解碼解決方案;
  3. 增長通道(channel)對象,做爲新的原始 I/O 抽象;
  4. 支持鎖和內存映射文件的文件訪問接口;
  5. 提供了基於 Selector 的異步網絡 I/O。

與流式的 I/O 不一樣,NIO是基於塊(Block)的,它以塊爲基本單位處理數據。在NIO中,最爲重要的兩個組件是緩衝 Buffer 和通道 Channel 。緩衝是一塊連續的內存塊,是 NIO 讀寫數據的中轉地。通道表示緩衝數據的源頭或者目的地,它用於向緩衝讀取或者寫入數據,是訪問緩衝的接口。
安全

本文主要是介紹經過NIO中的Buffer和Channel,來提高系統性能。性能優化

1. NIO的Buffer類族和Channel

在NIO的實現中,Buffer是一個抽象類。JDK爲每一種 Java 原生類型都建立了一個Buffer,如圖網絡

在NIO中和Buffer配合使用的還有 Channel 。Channel 是一個雙向通道,便可讀又可寫。app

下面列出Java NIO中最重要的集中Channel的實現:異步

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel用於文件的數據讀寫。 DatagramChannel用於UDP的數據讀寫。 SocketChannel用於TCP的數據讀寫。 ServerSocketChannel容許咱們監聽TCP連接請求,每一個請求會建立會一個SocketChannel.函數

應用程序只能經過Buffer對Channel進行讀寫。好比,在讀一個Channel的時候,須要先將數據讀入到相應的Buffer,而後在Buffer中進行讀取。工具

一個使用NIO進行文件複製的例子以下:

@Test
    public void test() throws IOException {
        //寫文件通道
        FileOutputStream fileOutputStream = new FileOutputStream(new File(path_copy));
        FileChannel wChannel = fileOutputStream.getChannel();
        
        //讀文件通道
        FileInputStream fileInputStream = new FileInputStream(new File(path));
        FileChannel rChannel = fileInputStream.getChannel();
        
        ByteBuffer byteBufferRead = ByteBuffer.allocate(1024);//從堆中分配緩衝區
        
        while(rChannel.read(byteBufferRead)!=-1){
            byteBufferRead.flip();//將Buffer從寫狀態切換到讀狀態
            while(byteBufferRead.hasRemaining()){
                wChannel.write(byteBufferRead);
            }
            byteBufferRead.clear();//爲讀入數據到Buffer作準備
        }
        wChannel.close();
        rChannel.close();
    }

2. Buffer的基本原理

buffer中有三個重要參數:位置(position)、容量(capacity)、上限(limit)。

  • 位置(position):當前緩衝區(Buffer)的位置,將從該位置日後讀或寫數據。
  • 容量(capacity):緩衝區的總容量上限。
  • 上限(limit):緩衝區的實際容量大小。

再回到上面的例子:

在建立ByteBuffer緩衝區實例後,位置(position)、容量(capacity)、上限(limit)均已初始化!position爲0,capacity、limit均爲最大長度值。rChannel 通道的 read() 方法會把文件數據寫入ByteBuffer緩衝區,此時position的位置移動到下一個即將輸入的位置,而limit,capacity不變。接着ByteBuffer緩衝區執行flip()操做,該操做會把會把limit移到position的位置,而且把position的位置重置爲0。這樣作是防止程序讀到根本沒有進行操做的區域。

接着 wChannel 通道的 write() 方法會讀取ByteBuffer緩衝區的數據到文件,和 read() 操做同樣,write() 操做也會設置position的位置到當前位置。爲了便於下次讀入數據到緩衝區,咱們調用clear()方法將position,capacity,limit初始化。

一、Buffer的建立

第一種從堆中建立

ByteBuffer byteBufferRead = ByteBuffer.allocate(1024);

從既有數組中建立

byte[] bytes = new byte[1024];
ByteBuffer byteBufferRead = ByteBuffer.wrap(bytes);

二、重置和清空緩衝區

Buffer提供了一些用於重置和清空 Buffer 狀態的函數,以下:

public final Buffer rewind()
public final Buffer clear()
public final Buffer flip()

rewind() 方法將position置零,並清除標誌位(mark)。做用是爲提取Buffer的有效數據作準備:

out.write(buf); //從buffer讀取數據寫入channel
buf.rewind();//回滾buffer
buf.get(array);//將buffer的有效數據複製到數組中

clear()方法將position置零,同時將limit設置爲capacity的大小,並清除了mark。爲從新寫Buffer作準備:

buf.clear();//爲讀入數據到Buffer作準備
ch.read(buf);

flip()方法先將limit設置到position的位置,而且把position的位置重置爲零,並清除mark。一般用於讀寫轉換。

三、標誌緩衝區

標誌(mark)緩衝區是一項在數據處理時比較有用的功能,它就像書籤同樣,能夠在數據處理過程當中。隨時記錄當前位置,而後在任意時刻,回到這個位置,從而加快或簡化數據處理流程。主要函數以下:

public final Buffer mark()
public final Buffer reset()

mark()方法用於記錄當前位置,reset()方法用於回到當前位置。

四、複製緩衝區

複製緩衝區是指以原緩衝區爲基礎,生成一個徹底同樣的新緩衝區。方法以下:

public abstract ByteBuffer duplicate()

簡單來講,複製生成的新的緩衝區與原緩衝區共享相同內存數據,每一方的數據改動都是相互可見的。可是,二者又維護了各自的position、limit和mark。這就大大增長了程序的靈活性,爲多方處理數據提供了可能。

五、緩衝區分片

緩存區分片使用slice()方法實現,它將在現有的緩衝區中,建立新的子緩衝區,子緩衝區和父緩衝區共享數據。

public abstract ByteBuffer slice()

新緩衝區的內容將今後緩衝區的當前位置開始。此緩衝區內容的更改在新緩衝區中是可見的,反之亦然;這兩個緩衝區的position、limit和mark是相互獨立的。 新緩衝區的position位置將爲零,其容量和limit將爲此緩衝區中所剩餘的字節數量,其mark標記是不肯定的。當且僅當此緩衝區爲只讀時,新緩衝區纔是只讀的。

六、只讀緩衝區

可使用緩衝區對象的asReadOnlyBuffer()方法獲得一個與當前緩衝區一致的,而且共享內存數據的只讀緩衝區。只讀緩衝區對於數據安全很是有用。若是不但願數據被隨意修改,返回一個只讀緩衝區是頗有幫助的。

public abstract ByteBuffer asReadOnlyBuffer()

七、文件映射到內存

NIO提供了一種將文件映射到內存的方法進行I/O操做,它能夠比常規的基於流的方式快不少。這個操做主要由FileChannel.map()方法實現。以下

MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

以上代碼將文件的前1024個字節映射到內存中。返回MappedByteBuffe,它是Buffer的子類,所以,能夠像使用ByteBuffer那樣使用它。

八、處理結構化數據(散射、彙集)

NIO提供處理結構化數據的方法,稱之爲散射(Scattering)和彙集(Gathering)。

散射是指將數據讀入一組數據中,而不只僅是一個。彙集與之相反。

在JDK中,經過GatheringByteChannel, ScatteringByteChannel接口提供相關操做。

下面我用一個示例來講明彙集寫於散射讀。

示例功能:寫入兩段話到文件,而後讀取打印。

@Test
    public void test() throws IOException {
        String path = "D:\\test.txt";
        //彙集寫
        //這是一組數據
        ByteBuffer byteBuffer1 = ByteBuffer.wrap("Java是最好的工具".getBytes(Charset.forName("UTF-8")));
        ByteBuffer byteBuffer2 = ByteBuffer.wrap("像風同樣".getBytes(Charset.forName("UTF-8")));
        //記錄數據長度
        int length1 = byteBuffer1.limit();
        int length2 = byteBuffer2.limit();
        //用 ByteBuffer 數組存放ByteBuffer實例的引用。
        ByteBuffer[] byteBuffers = new ByteBuffer[]{byteBuffer1, byteBuffer2};
        //獲取文件寫通道
        FileOutputStream fileOutputStream = new FileOutputStream(new File(path));
        FileChannel channel = fileOutputStream.getChannel();
        //開始寫
        channel.write(byteBuffers);
        channel.close();
        
        //散射讀
        byteBuffer1 = ByteBuffer.allocate(length1);
        byteBuffer2 = ByteBuffer.allocate(length2);
        byteBuffers = new ByteBuffer[]{byteBuffer1,byteBuffer2};
        //獲取文件讀通道
        FileInputStream fileInputStream = new FileInputStream(new File(path));
        channel = fileInputStream.getChannel();
        //開始讀
        channel.read(byteBuffers);
        //讀取
        System.out.println(new String(byteBuffers[0].array(),"utf-8"));
        System.out.println(new String(byteBuffers[1].array(),"utf-8"));
    }

執行完成後,咱們打開test.txt文件,看到:Java是最好的工具像風同樣

並在控制檯打印出:

Java是最好的工具
像風同樣

九、直接內存訪問

NIO的 Buffer 還提供了一個能夠直接訪問系統物理內存的類----DirectByteBuffer。

DirectByteBuffer繼承自ByteBuffer,但和普通Buffer不一樣。普通的ByteBuffer仍然在JVM堆上分配空間,其最大內存受到最大堆的限制。而DirectByteBuffer直接分配在物理內存中,並不佔用堆空間。並且,DirectByteBuffer是一種更加接近系統底層的方法,因此,它的速度比普通的ByteBuffer更快。

使用很簡單,只須要把 ByteBuffer.allocate(1024) 換成 ByteBuffer.allocateDirect(1024) 便可。該方法的源碼爲

public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

有必要說明的是,使用參數-XX:MaxDirectMemorySize=10M 能夠指定DirectByteBuffer的大小最可能是 10M。

DirectByteBuffer的讀寫比普通Buffer快,但建立和銷燬卻比普通Buffer慢。但若是能將DirectByteBuffer進行復用,那麼,在讀寫頻繁的狀況下,它能夠大幅改善系統性能。

3.與傳統I/O的對比

I/O和NIO的最大區別就是 傳統I/O是面向(緩衝)流,NIO是面向緩衝區

使用 Java 進行 I/O操做有兩種基本方法:

  1. 使用基於InputStream 和 OutputStream 的方式;(字節流)
  2. 使用 Writer 和 Reader。(字符流)

不管使用哪一種方式進行文件 I/O,若是能合理地使用緩衝,就能有效的提升I/O的性能。

用傳統I/O實現剛開始的文件複製例子,代碼以下:

@Test
    public void test6() throws IOException {
        //緩衝輸出流
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(new File(path_copy)));
        //緩衝輸入流
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(new File(path)));
        byte[] bytes = new byte[1024];
        while (bufferedInputStream.read(bytes) != -1) {
            bufferedOutputStream.write(bytes);
        }
        bufferedInputStream.close();
        bufferedOutputStream.close();
    }

須要注意的是,雖然使用ByteBuffer讀寫文件比Stream快不少,但不足以代表二者存在很如此之大的差距。這其中,因爲ByteBuffer是將文件一次性讀入內存再作後續處理,而Stream方式是則是邊讀文件邊處理數據(雖然使用了緩衝組件 BufferedInputStream),這也是致使二者性能差別的緣由之一。雖如此,仍不能掩蓋使用NIO的優點。使用NIO替代傳統I/O操做,對系統總體性能的優化,應該是有立竿見影的效果的。


附錄

位:"位(bit)"是電子計算機中最小的數據單位。每一位的狀態只能是0或1。

字節:8個二進制位構成1個"字節(Byte)",它是存儲空間的基本計量單位。1個字節能夠儲存1個英文字母或者半個漢字,換句話說,1個漢字佔據2個字節的存儲空間。

以1KB的文件舉例:

1Byte = 8Bit
1KB = 1024Byte

當咱們進行byte[] bytes = new byte[1024]操做時,至關於開闢了1KB的內存空間。

參考

Java程序性能優化 葛一鳴著

相關文章
相關標籤/搜索