Java nio記錄

mono7發表於2020-11-29

記憶體對映檔案

JAVA處理大檔案,一般用BufferedReader,BufferedInputStream這類帶緩衝的IO類,不過如果檔案超大的話,更快的方式是採用MappedByteBuffer。

MappedByteBuffer是NIO引入的檔案記憶體對映方案,讀寫效能極高。NIO最主要的就是實現了對非同步操作的支援。其中一種通過把一個套接字通道(SocketChannel)註冊到一個選擇器(Selector)中,不時呼叫後者的選擇(select)方法就能返回滿足的選擇鍵(SelectionKey),鍵中包含了SOCKET事件資訊。這就是select模型。

SocketChannel的讀寫是通過一個類叫ByteBuffer來操作的.這個類本身的設計是不錯的,比直接操作byte[]方便多了. ByteBuffer有兩種模式:直接/間接.間接模式最典型(也只有這麼一種)的就是HeapByteBuffer,即操作堆記憶體 (byte[]).但是記憶體畢竟有限,如果我要傳送一個1G的檔案怎麼辦?不可能真的去分配1G的記憶體.這時就必須使用”直接”模式,即 MappedByteBuffer,檔案對映.

先中斷一下,談談作業系統的記憶體管理.一般作業系統的記憶體分兩部分:實體記憶體;虛擬記憶體.虛擬記憶體一般使用的是頁面映像檔案,即硬碟中的某個(某些)特殊的檔案.作業系統負責頁面檔案內容的讀寫,這個過程叫”頁面中斷/切換”. MappedByteBuffer也是類似的,你可以把整個檔案(不管檔案有多大)看成是一個ByteBuffer.MappedByteBuffer 只是一種特殊的ByteBuffer,即是ByteBuffer的子類。 MappedByteBuffer 將檔案直接對映到記憶體(這裡的記憶體指的是虛擬記憶體,並不是實體記憶體)。通常,可以對映整個檔案,如果檔案比較大的話可以分段進行對映,只要指定檔案的那個部分就可以。

概念

FileChannel提供了map方法來把檔案影射為記憶體映像檔案: MappedByteBuffer map(int mode,long position,long size); 可以把檔案的從position開始的size大小的區域對映為記憶體映像檔案,mode指出了 可訪問該記憶體映像檔案的方式:

  • READ_ONLY,(只讀): 試圖修改得到的緩衝區將導致丟擲 ReadOnlyBufferException.(MapMode.READ_ONLY)
  • READ_WRITE(讀/寫): 對得到的緩衝區的更改最終將傳播到檔案;該更改對對映到同一檔案的其他程式不一定是可見的。 (MapMode.READ_WRITE)
  • PRIVATE(專用): 對得到的緩衝區的更改不會傳播到檔案,並且該更改對對映到同一檔案的其他程式也不是可見的;相反,會建立緩衝區已修改部分的專用副本。 (MapMode.PRIVATE)

MappedByteBuffer是ByteBuffer的子類,其擴充了三個方法:

  • force():緩衝區是READ_WRITE模式下,此方法對緩衝區內容的修改強行寫入檔案;
  • load():將緩衝區的內容載入記憶體,並返回該緩衝區的引用;
  • isLoaded():如果緩衝區的內容在實體記憶體中,則返回真,否則返回假;

案例對比

這裡通過採用ByteBuffer和MappedByteBuffer分別讀取大小約為5M的檔案”src/1.ppt”來比較兩者之間的區別,method3()是採用MappedByteBuffer讀取的,method4()對應的是ByteBuffer。

public static void method4(){
    RandomAccessFile aFile = null;
    FileChannel fc = null;
    try{
        aFile = new RandomAccessFile("src/1.ppt","rw");
        fc = aFile.getChannel();

        long timeBegin = System.currentTimeMillis();
        ByteBuffer buff = ByteBuffer.allocate((int) aFile.length());
        buff.clear();
        fc.read(buff);
        //System.out.println((char)buff.get((int)(aFile.length()/2-1)));
        //System.out.println((char)buff.get((int)(aFile.length()/2)));
        //System.out.println((char)buff.get((int)(aFile.length()/2)+1));
        long timeEnd = System.currentTimeMillis();
        System.out.println("Read time: "+(timeEnd-timeBegin)+"ms");

    }catch(IOException e){
        e.printStackTrace();
    }finally{
        try{
            if(aFile!=null){
                aFile.close();
            }
            if(fc!=null){
                fc.close();
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

public static void method3(){
    RandomAccessFile aFile = null;
    FileChannel fc = null;
    try{
        aFile = new RandomAccessFile("src/1.ppt","rw");
        fc = aFile.getChannel();
        long timeBegin = System.currentTimeMillis();
        MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, aFile.length());
        // System.out.println((char)mbb.get((int)(aFile.length()/2-1)));
        // System.out.println((char)mbb.get((int)(aFile.length()/2)));
        //System.out.println((char)mbb.get((int)(aFile.length()/2)+1));
        long timeEnd = System.currentTimeMillis();
        System.out.println("Read time: "+(timeEnd-timeBegin)+"ms");
    }catch(IOException e){
        e.printStackTrace();
    }finally{
        try{
            if(aFile!=null){
                aFile.close();
            }
            if(fc!=null){
                fc.close();
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}
method3();
System.out.println("=============");
method4();

輸出結果(執行在普通PC機上):

Read time: 2ms
=============
Read time: 12ms

通過輸出結果可以看出彼此的差別,一個例子也許是偶然,那麼下面把5M大小的檔案替換為200M的檔案,輸出結果:

Read time: 1ms
=============
Read time: 407ms

可以看到差距拉大。

MappedByteBuffer有資源釋放的問題:被MappedByteBuffer開啟的檔案只有在垃圾收集時才會被關閉,而這個點是不確定的。在Javadoc中這裡描述:A mapped byte buffer and the file mapping that it represents remian valid until the buffer itself is garbage-collected。

Scatter/Gatter

分散(scatter)從Channel中讀取是指在讀操作時將讀取的資料寫入多個buffer中。因此,Channel將從Channel中讀取的資料“分散(scatter)”到多個Buffer中。

聚集(gather)寫入Channel是指在寫操作時將多個buffer的資料寫入同一個Channel,因此,Channel 將多個Buffer中的資料“聚集(gather)”後傳送到Channel。

scatter / gather經常用於需要將傳輸的資料分開處理的場合,例如傳輸一個由訊息頭和訊息體組成的訊息,你可能會將訊息體和訊息頭分散到不同的buffer中,這樣你可以方便的處理訊息頭和訊息體。

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;

public class ScattingAndGather
{
    public static void main(String args[]){
        gather();
    }

    public static void gather()
    {
        ByteBuffer header = ByteBuffer.allocate(10);
        ByteBuffer body = ByteBuffer.allocate(10);

        byte [] b1 = {'0', '1'};
        byte [] b2 = {'2', '3'};
        header.put(b1);
        body.put(b2);

        ByteBuffer [] buffs = {header, body};

        try
        {
            FileOutputStream os = new FileOutputStream("src/scattingAndGather.txt");
            FileChannel channel = os.getChannel();
            channel.write(buffs);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}

transferFrom & transferTo
FileChannel的transferFrom()方法可以將資料從源通道傳輸到FileChannel中。

public static void method1(){
    RandomAccessFile fromFile = null;
    RandomAccessFile toFile = null;
    try
    {
        fromFile = new RandomAccessFile("src/fromFile.xml","rw");
        FileChannel fromChannel = fromFile.getChannel();
        toFile = new RandomAccessFile("src/toFile.txt","rw");
        FileChannel toChannel = toFile.getChannel();

        long position = 0;
        long count = fromChannel.size();
        System.out.println(count);
        toChannel.transferFrom(fromChannel, position, count);

    }
    catch (IOException e)
    {
        e.printStackTrace();
    }
    finally{
        try{
            if(fromFile != null){
                fromFile.close();
            }
            if(toFile != null){
                toFile.close();
            }
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }
}

方法的輸入引數position表示從position處開始向目標檔案寫入資料,count表示最多傳輸的位元組數。如果源通道的剩餘空間小於 count 個位元組,則所傳輸的位元組數要小於請求的位元組數。此外要注意,在SoketChannel的實現中,SocketChannel只會傳輸此刻準備好的資料(可能不足count位元組)。因此,SocketChannel可能不會將請求的所有資料(count個位元組)全部傳輸到FileChannel中。

transferTo()方法將資料從FileChannel傳輸到其他的channel中。

public static void method2()
{
    RandomAccessFile fromFile = null;
    RandomAccessFile toFile = null;
    try
    {
        fromFile = new RandomAccessFile("src/fromFile.txt","rw");
        FileChannel fromChannel = fromFile.getChannel();
        toFile = new RandomAccessFile("src/toFile.txt","rw");
        FileChannel toChannel = toFile.getChannel();


        long position = 0;
        long count = fromChannel.size();
        System.out.println(count);
        fromChannel.transferTo(position, count,toChannel);

    }
    catch (IOException e)
    {
        e.printStackTrace();
    }
    finally{
        try{
            if(fromFile != null){
                fromFile.close();
            }
            if(toFile != null){
                toFile.close();
            }
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }
}