轉載自: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資源釋放問題
FileChannel.map
方法創建的。映射的字節緩沖區和它所表示的文件映射關系在該緩沖區本身成為垃圾回收緩沖區之前一直保持有效。此類用特定於內存映射文件區域的操作擴展
ByteBuffer
類。 這個類本身的設計是不錯的,比直接操作byte[]方便多了。
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 }
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的派生。畢竟這是一個很實用的類,如果允許派生,那么我就可以操作的就不僅僅限於堆內存和文件了,我可以擴展到任何存儲設備。