1. 程式人生 > >Java性能優化之使用NIO提升性能

Java性能優化之使用NIO提升性能

() err buffer 由於 方式 網絡 容量 文件復制 狀態

在軟件系統中,由於IO的速度要比內存慢,因此,I/O讀寫在很多場合都會成為系統的瓶頸。提升I/O速度,對提升系統整體性能有著很大的好處。

在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 是一個雙向通道,即可讀又可寫。

應用程序只能通過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均為最大長度值。當經過讀文件通道的 read() 寫入之後,position的位置移動到下一個即將輸入的位置,而limit,capacity不變。接著執行flip()操作,該操作會把會把limit移到position的位置,並且把position的位置重置為0。這樣做是防止程序讀到根本沒有進行操作的區域。

然後寫文件通道讀取ByteBuffer緩沖區的數據,和寫操作一樣,讀操作也會設置position的位置到當前位置。為了便於下次讀入數據到緩沖區,我們調用clear()方法將position,capacity,limit初始化。

1、Buffer的創建

第一種從堆中創建

ByteBuffer byteBufferRead = ByteBuffer.allocate(1024);

從既有數組中創建

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

2、重置和清空緩沖區

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。通常用於讀寫轉換。

3、標誌緩沖區

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

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

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

4、復制緩沖區

復制緩沖區是指以原緩沖區為基礎,生成一個完全一樣的新緩沖區。方法如下:

public abstract ByteBuffer duplicate()

簡單來說,復制生成的新的緩沖區與原緩沖區共享相同內存數據,每一方的數據改動都是相互可見的。但是,兩者又維護了各自的position、limit和mark。這就大大增加了程序的靈活性,為多方處理數據提供了可能。

5、緩沖區分片

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

public abstract ByteBuffer slice()

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

6、只讀緩沖區

可以使用緩沖區對象的asReadOnlyBuffer()方法得到一個與當前緩沖區一致的,並且共享內存數據的只讀緩沖區。只讀緩沖區對於數據安全非常有用。如果不希望數據被隨意修改,返回一個只讀緩沖區是很有幫助的。

public abstract ByteBuffer asReadOnlyBuffer()

7、文件映射到內存

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

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

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

8、處理結構化數據

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是最好的工具
像風一樣

9、直接內存訪問

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實現剛開始的文件復制例子,代碼如下:

    @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操作,對系統整體性能的優化,應該是有立竿見影的效果的。


附:應用程序的byte和文件大小KB到底啥關系?

這裏普及一下,我們在電腦可以看到文件的大小是多少KB,MB等等,而我們在系統中是使用基本數據類型byte對數據進行操作。所以byte和KB到底啥關系,有必要了解一下。拿1KB的文件舉例:

1Byte = 8Bit
1KB = 1024Bit
1KB = 128Byte

當我們進行byte[] bytes = new byte[1024]操作時,相當於開辟了8KB的內存空間。

Java性能優化之使用NIO提升性能