一、什麼是內存映射文件
內存映射文件,是由一個文件到一塊內存的映射,可以理解爲將一個文件映射到進程地址,然後可以通過操作內存來訪問文件數據。說白了就是使用虛擬內存將磁盤的文件數據加載到虛擬內存的內存頁,然後就可以直接操作內存頁數據。
我們讀寫一個文件使用read()和write()方法,這兩個方法是調用系統底層接口來傳輸數據,因爲內核空間的文件頁和用戶空間的緩衝區沒有一一對應,所以讀寫數據時會在內核空間和用戶空間之間進行數據拷貝,在操作大量文件數據時會導致性能很低,使用內存映射文件可以非常高效的操作大量文件數據。
通過內存映射機制操作文件比使用常規方法和使用FileChannel讀寫高效的多。
內存映射文件使用文件系統建立從用戶空間到可用文件系統頁的虛擬內存映射,這樣做有以下好處:
- 用戶進程把文件數據當內存數據,無需調用read()或write()
- 當用戶進程接觸到映射內存空間,會自動產生頁錯誤,從而將文件數據從磁盤讀到內存;若用戶空間進程修改了內存頁數據,相關頁會自動標記並刷新到磁盤,文件被更新
- 操作系統的虛擬內存對內存頁進行高速緩存,自動根據系統負載進行內存管理
- 用戶空間和內核空間的數據總是一一對應,無需執行緩衝區拷貝
- 大數據的文件使用映射,無需消耗大量內存即可進行數據拷貝
二、如何創建內存映射文件
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
FileChannel fc = raf.getChannel();
//將test.txt文件所有數據映射到虛擬內存,並只讀
MappedByteBuffer mbuff = fc.map(MapMode.READ_ONLY, 0, fc.size());
映射文件的範圍不應超過文件的實際大小,否則文件的大小會被增大到指定的大小
//實際文件只有10個字節,執行下面代碼後,文件內容變爲10000個字節
MappedByteBuffer mbuff = fc.map(MapMode.READ_WRITE, 0, 10000);
第一個參數MapMode是個三個值:
- MapMode.READ_ONLY:只讀,若FileChannel不可讀,拋出NonReadableChannelException
- MapMode.READ_WRITE:可讀寫,若FileChannel不可寫,拋出NonWritableChannelException
- MapMode.PRIVATE:創建一個寫時拷貝的映射,修改映射內存頁的數據只對MappedByteBuffer可視
三、MappedByteBuffer API
MappedByteBuffer是ByteBuffer的子類,所以可被通道讀寫。MappedByteBuffer提供的方法:
- load():加載整個文件到內存
- isLoaded():判斷文件數據是否全部加載到了內存
- force():將緩衝區的更改刷新到磁盤
四、通道到通道傳輸
將文件數據從一個通道傳輸到另一個通道,FileChannel提供下面2個高效方法:
- transferTo(long position, long count, WritableByteChannel target):傳輸到哪個可寫通道
- transferFrom(ReadableByteChannel src, long position, long count):從哪個可讀通道傳輸過來
通道到通道傳輸數據使用上面2個方法不需要使用中間緩衝區。只有FileChannel有以上方法,所以Channel-to-Channel中必須有一個是FileChannel。對於傳輸大量數據,以上2個方法的效率是非常高的。
下面是個例子
//獲取test0.txt文件的通道句柄
RandomAccessFile raf0 = new RandomAccessFile("test0.txt", "r");
FileChannel fc0 = raf0.getChannel();
//獲取test1.txt文件的通道句柄
RandomAccessFile raf1 = new RandomAccessFile("test1.txt", "rw");
FileChannel fc1 = raf1.getChannel();
//將test0.txt傳輸到test1.txt
fc0.transferTo(0, fc0.size(), fc1);
//強制刷新數據到磁盤
fc0.force(true);
//關閉通道
raf1.close();
raf0.close();