java大文件讀寫操作,java nio 之MappedByteBuffer,高效文件/內存映射


轉載自:http://langgufu.iteye.com/blog/2107023

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. 隨時隨地寫入

下面來看代碼:

 1 package study;  
 2 import java.io.FileInputStream;  
 3 import java.io.FileOutputStream;  
 4 import java.nio.ByteBuffer;  
 5 import java.nio.MappedByteBuffer;  
 6 import java.nio.channels.FileChannel;  
 7   
 8 public class MapMemeryBuffer {  
 9   
10     public static void main(String[] args) throws Exception {  
11         ByteBuffer byteBuf = ByteBuffer.allocate(1024 * 14 * 1024);  
12         byte[] bbb = new byte[14 * 1024 * 1024];  
13         FileInputStream fis = new FileInputStream("e://data/other/UltraEdit_17.00.0.1035_SC.exe");  
14         FileOutputStream fos = new FileOutputStream("e://data/other/outFile.txt");  
15         FileChannel fc = fis.getChannel();  
16         long timeStar = System.currentTimeMillis();// 得到當前的時間  
17         fc.read(byteBuf);// 1 讀取  
18         //MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());  
19         System.out.println(fc.size()/1024);  
20         long timeEnd = System.currentTimeMillis();// 得到當前的時間  
21         System.out.println("Read time :" + (timeEnd - timeStar) + "ms");  
22         timeStar = System.currentTimeMillis();  
23         fos.write(bbb);//2.寫入  
24         //mbb.flip();  
25         timeEnd = System.currentTimeMillis();  
26         System.out.println("Write time :" + (timeEnd - timeStar) + "ms");  
27         fos.flush();  
28         fc.close();  
29         fis.close();  
30     }  
31   
32 }  
33 運行結果:  
34 14235  
35 Read time :24ms  
36 Write time :21ms  
37 我們把標注1和2語句注釋掉,換成它們下面的被注釋的那條語句,再來看運行效果。14235  
38 Read time :2ms  
39 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實現文件復制功能非常容易,可以用以下方法來實現。
 1 //文件復制
 2    public void copyFile(String filename,String srcpath,String destpath)throws IOException {
 3     File source = new File(srcpath+"/"+filename);
 4     File dest = new File(destpath+"/"+filename);
 5      FileChannel in = null, out = null;
 6      try { 
 7       in = new FileInputStream(source).getChannel();
 8       out = new FileOutputStream(dest).getChannel();
 9       long size = in.size();
10       MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
11       out.write(buf);
12       in.close();
13       out.close();
14       source.delete();//文件復制完成后,刪除源文件
15      }catch(Exception e){
16       e.printStackTrace();
17      } finally {
18       in.close();
19       out.close();
20      }
21    }
但是如果要實現文件文件復制完成后,刪除源文件,以上方法就有問題。因為在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的網友提出了一個解決方法,我也測試過,可以實現期望的功能。具體實現代碼如下:
 1 public static void clean(final Object buffer) throws Exception {
 2          AccessController.doPrivileged(new PrivilegedAction() {
 3              public Object run() {
 4              try {
 5                 Method getCleanerMethod = buffer.getClass().getMethod("cleaner",new Class[0]);
 6                 getCleanerMethod.setAccessible(true);
 7                 sun.misc.Cleaner cleaner =(sun.misc.Cleaner)getCleanerMethod.invoke(buffer,new Object[0]);
 8                 cleaner.clean();
 9              } catch(Exception e) {
10                 e.printStackTrace();
11              }
12                 return null;}});
13          
14 }

不知道為什么SUN不提供ByteBuffer的派生。畢竟這是一個很實用的類,如果允許派生,那么我就可以操作的就不僅僅限於堆內存和文件了,我可以擴展到任何存儲設備。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM