1. 程式人生 > >java大檔案讀寫操作,java nio 之MappedByteBuffer,高效檔案/記憶體對映

java大檔案讀寫操作,java nio 之MappedByteBuffer,高效檔案/記憶體對映

原文地址:https://www.cnblogs.com/lyftest/p/6564282.html

 

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

 MappedByteBuffer是java nio引入的檔案記憶體對映方案,讀寫效能極高。NIO最主要的就是實現了對非同步操作的支援。其中一種通過把一個套接字通道(SocketChannel)註冊到一個選擇器(Selector)中,不時呼叫後者的選擇(select)方法就能返回滿足的選擇鍵(SelectionKey),鍵中包含了SOCKET事件資訊。這就是select模型。
    SocketChannel的讀寫是通過一個類叫ByteBuffer(java.nio.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,READ_WRITE,PRIVATE.                    
a. READ_ONLY,(只讀): 試圖修改得到的緩衝區將導致丟擲 ReadOnlyBufferException.(MapMode.READ_ONLY)
 b. READ_WRITE(讀/寫): 對得到的緩衝區的更改最終將傳播到檔案;該更改對對映到同一檔案的其他程式不一定是可見的。 (MapMode.READ_WRITE)
c. PRIVATE(專用): 對得到的緩衝區的更改不會傳播到檔案,並且該更改對對映到同一檔案的其他程式也不是可見的;相反,會建立緩衝區已修改部分的專用副本。 (MapMode.PRIVATE)

三個方法:

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

三個特性:

    呼叫通道的map()方法後,即可將檔案的某一部分或全部對映到記憶體中,對映記憶體緩衝區是個直接緩衝區,繼承自ByteBuffer,但相對於ByteBuffer,它有更多的優點:

a. 讀取快
b. 寫入快
c. 隨時隨地寫入

下面來看程式碼:

package study;  
import java.io.FileInputStream;  
import java.io.FileOutputStream;  
import java.nio.ByteBuffer;  
import java.nio.MappedByteBuffer;  
import java.nio.channels.FileChannel;  
  
public class MapMemeryBuffer {  
  
    public static void main(String[] args) throws Exception {  
        ByteBuffer byteBuf = ByteBuffer.allocate(1024 * 14 * 1024);  
        byte[] bbb = new byte[14 * 1024 * 1024];  
        FileInputStream fis = new FileInputStream("e://data/other/UltraEdit_17.00.0.1035_SC.exe");  
        FileOutputStream fos = new FileOutputStream("e://data/other/outFile.txt");  
        FileChannel fc = fis.getChannel();  
        long timeStar = System.currentTimeMillis();// 得到當前的時間  
        fc.read(byteBuf);// 1 讀取  
        //MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());  
        System.out.println(fc.size()/1024);  
        long timeEnd = System.currentTimeMillis();// 得到當前的時間  
        System.out.println("Read time :" + (timeEnd - timeStar) + "ms");  
        timeStar = System.currentTimeMillis();  
        fos.write(bbb);//2.寫入  
        //mbb.flip();  
        timeEnd = System.currentTimeMillis();  
        System.out.println("Write time :" + (timeEnd - timeStar) + "ms");  
        fos.flush();  
        fc.close();  
        fis.close();  
    }  
  
}  
執行結果:  
14235  
Read time :24ms  
Write time :21ms  
我們把標註1和2語句註釋掉,換成它們下面的被註釋的那條語句,再來看執行效果。14235  
Read time :2ms  
Write time :0ms

可以看出速度有了很大的提升。MappedByteBuffer的確快,但也存在一些問題,主要就是記憶體佔用和檔案關閉等不確定問題。被MappedByteBuffer開啟的檔案只有在垃圾收集時才會被關閉,而這個點是不確定的。在javadoc裡是這麼說的:A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself  is garbage-collected.
這裡提供一種解決方案:

AccessController.doPrivileged(new PrivilegedAction() {  
  public Object run() {  
    try {  
      Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);  
      getCleanerMethod.setAccessible(true);  
      sun.misc.Cleaner cleaner = (sun.misc.Cleaner)   
      getCleanerMethod.invoke(byteBuffer, new Object[0]);  
      cleaner.clean();  
    } catch (Exception e) {  
      e.printStackTrace();  
    }  
    return null;  
  }  
});

 

關於MappedByteBuffer資源釋放問題

JDK1.4中加入了一個新的包:NIO(java.nio.*)。這個庫最大的功能(我認為)就是增加了對非同步套接字的支援。其實在 其他語言中,包括在最原始的SOCKET實現(BSD SOCKET),這是一個早有的功能:非同步回撥讀/寫事件,通過選擇器動態選擇感興趣的事件,等等。

先談談作業系統的記憶體管理。一般作業系統的記憶體分兩部分:實體記憶體;虛擬記憶體。虛擬記憶體一般使用的是頁面映像檔案,即硬碟中的某個(某些)特殊的檔案.作業系統負責頁面檔案內容的讀寫,這個過程叫"頁面中斷/切換"。

MappedByteBuffer也是類似的,你可以把整個檔案(不管檔案有多大)看成是一個ByteBuffer。這是一個很好的設計,除了令人頭疼的一點在後面會講到。

java.lang.Object
   java.nio.Buffer
      java.nio.ByteBuffer
          java.nio.MappedByteBuffer

MappedByteBuffer是一個比較方便使用的類。其內容是檔案的記憶體對映區域。對映的位元組緩衝區是通過FileChannel.map 方法建立的。對映的位元組緩衝區和它所表示的檔案對映關係在該緩衝區本身成為垃圾回收緩衝區之前一直保持有效。此類用特定於記憶體對映檔案區域的操作擴充套件 ByteBuffer 類。 這個類本身的設計是不錯的,比直接操作byte[]方便多了。

ByteBuffer有兩種模式:直接/間接。間接模式最典型(也只有這麼一種)的就是HeapByteBuffer,即操作堆記憶體(byte [])。但是記憶體畢竟有限,如果我要傳送一個1G的檔案怎麼辦?不可能真的去分配1G的記憶體.這時就必須使用"直接"模式,即 MappedByteBuffer,檔案對映。

在JDK API文件中這樣描述的:

全部或部分對映的位元組緩衝區可能隨時成為不可訪問的,例如,如果我們擷取對映的檔案。試圖訪問對映的位元組緩衝區的不可訪問區域將不會更改緩衝區 的內容,並導致在訪問時或訪問後的某個時刻丟擲未指定的異常。因此強烈推薦採取適當的預防措施,以避免此程式或另一個同時執行的程式對對映的檔案執行操作 (讀寫檔案內容除外)。

MappedByteBuffer只能通過呼叫FileChannel的map()取得,再沒有其他方式.但是令人奇怪的是,SUN提供了map()卻沒有提供unmap().這樣會導致什麼後果呢?

這樣,問題就出現了。通過MappedByteBuffer實現檔案複製功能非常容易,可以用以下方法來實現。

//檔案複製
   public void copyFile(String filename,String srcpath,String destpath)throws IOException {
    File source = new File(srcpath+"/"+filename);
    File dest = new File(destpath+"/"+filename);
     FileChannel in = null, out = null;
     try { 
      in = new FileInputStream(source).getChannel();
      out = new FileOutputStream(dest).getChannel();
      long size = in.size();
      MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
      out.write(buf);
      in.close();
      out.close();
      source.delete();//檔案複製完成後,刪除原始檔
     }catch(Exception e){
      e.printStackTrace();
     } finally {
      in.close();
      out.close();
     }
   }

但是如果要實現檔案檔案複製完成後,刪除原始檔,以上方法就有問題。因為在source.delete()時,會返回false,刪除失敗,主 要原因是變數buf仍然有原始檔的控制代碼,檔案處於不可刪除狀態。既然MappedByteBuffer是從FileChannel中map()出來的,為 什麼它又不提供unmap()呢?SUN自己也沒有講清楚為什麼。O'Reilly的<<Java NIO>>中說是因為"安全"的原因,但是到底unmap()會怎麼不安全,作者也沒有講清楚。

在sun網站也有相應的BUG報告:bug id:4724038連結為http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4724038,但是sun自己不認為是BUG,而只是一個RFE(Request For Enhancement),有待改進。

好在有個叫bellomi的網友提出了一個解決方法,我也測試過,可以實現期望的功能。具體實現程式碼如下:

public static void clean(final Object buffer) throws Exception {
         AccessController.doPrivileged(new PrivilegedAction() {
             public Object run() {
             try {
                Method getCleanerMethod = buffer.getClass().getMethod("cleaner",new Class[0]);
                getCleanerMethod.setAccessible(true);
                sun.misc.Cleaner cleaner =(sun.misc.Cleaner)getCleanerMethod.invoke(buffer,new Object[0]);
                cleaner.clean();
             } catch(Exception e) {
                e.printStackTrace();
             }
                return null;}});
         
}