MappedByteBuffer 是Java NIO中引入的一種硬盤物理文件和內(nèi)存映射方式胀糜,當物理文件較大時观蜗,采用MappedByteBuffer咧栗,讀寫性能較高逆甜,其內(nèi)部的核心實現(xiàn)是DirectByteBuffer(JVM 堆外直接物理內(nèi)存)。
JVM 進程通過內(nèi)存映射方式加載的物理文件并不會耗費同等大小的物理內(nèi)存致板。當應用程序訪問數(shù)據(jù)時交煞,程序通過虛擬地址尋址對應的內(nèi)存頁,如果物理內(nèi)存中不存在對應頁斟或,MMU則會產(chǎn)生缺頁中斷異常素征,CPU嘗試從系統(tǒng)Swap分區(qū)中查找,如仍不存在,則會直接從硬盤中物理文件中讀取御毅。
傳統(tǒng)的基于文件流的方式讀取文件方式是系統(tǒng)指令調(diào)用根欧,文件數(shù)據(jù)首先會被讀取到進程的內(nèi)核空間的緩沖區(qū),而后復制到進程的用戶空間端蛆,這個過程中存在兩次數(shù)據(jù)拷貝咽块;而內(nèi)存映射方式讀取文件的方式,也是系統(tǒng)指令調(diào)用欺税,在產(chǎn)生缺頁中斷后侈沪,CPU直接從磁盤文件load數(shù)據(jù)到進程的用戶空間,只有一次數(shù)據(jù)拷貝晚凿。
FileChannel提供了map方法把磁盤文件映射到虛擬內(nèi)存亭罪,通常情況可以映射整個文件,如果文件比較大歼秽,可以進行分段映射应役。
內(nèi)存映像文件訪問的方式,共三種:
a) MapMode.READ_ONLY:只讀燥筷,試圖修改得到的緩沖區(qū)將導致拋出異常箩祥。? ? b) MapMode.READ_WRITE:讀/寫,對得到的緩沖區(qū)的更改最終將寫入文件肆氓;但該更改對映射到同一文件的其他程序不一定是可見的袍祖。? ? c) MapMode.PRIVATE:私用,可讀可寫,但是修改的內(nèi)容不會寫入文件谢揪,只是buffer自身的改變蕉陋。
MappedByteBuffer在處理大文件時的確性能很高,但也存在一些問題拨扶,其所對應的內(nèi)存使用的是JVM堆外內(nèi)存凳鬓,JVM young gc和CMS gc并不能觸發(fā)回收MappedByteBuffer對應的內(nèi)存,只有full gc(stop the world的方式)可以使其回收內(nèi)存患民,堆外直接內(nèi)存會根據(jù)自己的情況(當需要新分配直接內(nèi)存時缩举,如果所剩堆外內(nèi)存空間不夠,第一次產(chǎn)生OutOfMemoryError時)來觸發(fā) System.gc()匹颤,此處有坑仅孩,若JVM配置了參數(shù)-XX:DisableExplicitGC,System.gc()將不會觸發(fā)full gc惋嚎,最終導致內(nèi)存泄漏杠氢。而且觸發(fā)其內(nèi)存回收的時間點是不確定的站刑。Java api文檔中標注:
在應用程序頻繁使用堆外內(nèi)存時另伍,還可以通過-XX:MaxDirectMemorySize來指定最大的堆外內(nèi)存大小,當使用達到了閾值的時候?qū)⒄{(diào)用System.gc來做一次full gc,以此來回收掉游離狀態(tài)的堆外內(nèi)存摆尝。
因此温艇,在使用堆外內(nèi)存高性能的福利的同時,及時的回收掉廢棄掉的內(nèi)存是十分關鍵的堕汞。
性能分析
從代碼層面上看勺爱,從硬盤上將文件讀入內(nèi)存,都要經(jīng)過文件系統(tǒng)進行數(shù)據(jù)拷貝讯检,并且數(shù)據(jù)拷貝操作是由文件系統(tǒng)和硬件驅(qū)動實現(xiàn)的琐鲁,理論上來說,拷貝數(shù)據(jù)的效率是一樣的人灼。
但是通過內(nèi)存映射的方法訪問硬盤上的文件围段,效率要比read和write系統(tǒng)調(diào)用高,這是為什么投放?
read()是系統(tǒng)調(diào)用奈泪,首先將文件從硬盤拷貝到內(nèi)核空間的一個緩沖區(qū),再將這些數(shù)據(jù)拷貝到用戶空間灸芳,實際上進行了兩次數(shù)據(jù)拷貝涝桅;
map()也是系統(tǒng)調(diào)用,但沒有進行數(shù)據(jù)拷貝烙样,當缺頁中斷發(fā)生時冯遂,直接將文件從硬盤拷貝到用戶空間,只進行了一次數(shù)據(jù)拷貝谒获。
所以债蜜,采用內(nèi)存映射的讀寫效率要比傳統(tǒng)的read/write性能高。
拷貝視頻代碼舉例:
機器配置: 內(nèi)存8G? ?CPU? 4核(i5-3210M)? ?
第一種方式:
long start = System.currentTimeMillis();
FileInputStream fis =new FileInputStream("d:\\追龍2.mp4");
FileChannel in = fis.getChannel();
FileOutputStream fos =new FileOutputStream("e:\\t.mp4");
FileChannel out = fos.getChannel();
out.transferFrom(in,0,in.size());
fis.close();
fos.close();
in.close();
out.close();
log.info(" 消耗時間:{} 秒",(System.currentTimeMillis()-start)/1000);
1.28G 大約消耗28秒時間
第二種方式:
long start = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("d:/追龍2.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("e:/追龍2.mp4"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//內(nèi)存映射文件
MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY,0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE,0, inChannel.size());
byte[] dst =new byte[1024];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
inMappedBuf.force();
outMappedBuf.force();
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
log.info("拷貝文件消耗時間{}",(end-start)/1000);
同樣1.28G 究反,消耗時時間不到1秒? ?
但是 寻定,第二種方式,拷貝的視頻文件精耐,不能播放狼速,不知道什么因素,如果有知道解決方案的卦停,麻煩給我留言一下或者email 80692072@qq.com? 謝謝向胡。