MappedByteBuffer

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)存大小,當使用達到了閾值的時候將調(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)和硬件驅動實現(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? 謝謝。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容