MappedByteBuffer 是Java NIO中引入的一種硬盤(pán)物理文件和內(nèi)存映射方式,當(dāng)物理文件較大時(shí),采用MappedByteBuffer,讀寫(xiě)性能較高,其內(nèi)部的核心實(shí)現(xiàn)是DirectByteBuffer(JVM 堆外直接物理內(nèi)存)。
JVM 進(jìn)程通過(guò)內(nèi)存映射方式加載的物理文件并不會(huì)耗費(fèi)同等大小的物理內(nèi)存。當(dāng)應(yīng)用程序訪問(wèn)數(shù)據(jù)時(shí),程序通過(guò)虛擬地址尋址對(duì)應(yīng)的內(nèi)存頁(yè),如果物理內(nèi)存中不存在對(duì)應(yīng)頁(yè),MMU則會(huì)產(chǎn)生缺頁(yè)中斷異常,CPU嘗試從系統(tǒng)Swap分區(qū)中查找,如仍不存在,則會(huì)直接從硬盤(pán)中物理文件中讀取。
傳統(tǒng)的基于文件流的方式讀取文件方式是系統(tǒng)指令調(diào)用,文件數(shù)據(jù)首先會(huì)被讀取到進(jìn)程的內(nèi)核空間的緩沖區(qū),而后復(fù)制到進(jìn)程的用戶空間,這個(gè)過(guò)程中存在兩次數(shù)據(jù)拷貝;而內(nèi)存映射方式讀取文件的方式,也是系統(tǒng)指令調(diào)用,在產(chǎn)生缺頁(yè)中斷后,CPU直接從磁盤(pán)文件load數(shù)據(jù)到進(jìn)程的用戶空間,只有一次數(shù)據(jù)拷貝。
FileChannel提供了map方法把磁盤(pán)文件映射到虛擬內(nèi)存,通常情況可以映射整個(gè)文件,如果文件比較大,可以進(jìn)行分段映射。
內(nèi)存映像文件訪問(wèn)的方式,共三種:
a) MapMode.READ_ONLY:只讀,試圖修改得到的緩沖區(qū)將導(dǎo)致拋出異常。? ? b) MapMode.READ_WRITE:讀/寫(xiě),對(duì)得到的緩沖區(qū)的更改最終將寫(xiě)入文件;但該更改對(duì)映射到同一文件的其他程序不一定是可見(jiàn)的。? ? c) MapMode.PRIVATE:私用,可讀可寫(xiě),但是修改的內(nèi)容不會(huì)寫(xiě)入文件,只是buffer自身的改變。
MappedByteBuffer在處理大文件時(shí)的確性能很高,但也存在一些問(wèn)題,其所對(duì)應(yīng)的內(nèi)存使用的是JVM堆外內(nèi)存,JVM young gc和CMS gc并不能觸發(fā)回收MappedByteBuffer對(duì)應(yīng)的內(nèi)存,只有full gc(stop the world的方式)可以使其回收內(nèi)存,堆外直接內(nèi)存會(huì)根據(jù)自己的情況(當(dāng)需要新分配直接內(nèi)存時(shí),如果所剩堆外內(nèi)存空間不夠,第一次產(chǎn)生OutOfMemoryError時(shí))來(lái)觸發(fā) System.gc(),此處有坑,若JVM配置了參數(shù)-XX:DisableExplicitGC,System.gc()將不會(huì)觸發(fā)full gc,最終導(dǎo)致內(nèi)存泄漏。而且觸發(fā)其內(nèi)存回收的時(shí)間點(diǎn)是不確定的。Java api文檔中標(biāo)注:
在應(yīng)用程序頻繁使用堆外內(nèi)存時(shí),還可以通過(guò)-XX:MaxDirectMemorySize來(lái)指定最大的堆外內(nèi)存大小,當(dāng)使用達(dá)到了閾值的時(shí)候?qū)⒄{(diào)用System.gc來(lái)做一次full gc,以此來(lái)回收掉游離狀態(tài)的堆外內(nèi)存。
因此,在使用堆外內(nèi)存高性能的福利的同時(shí),及時(shí)的回收掉廢棄掉的內(nèi)存是十分關(guān)鍵的。
性能分析
從代碼層面上看,從硬盤(pán)上將文件讀入內(nèi)存,都要經(jīng)過(guò)文件系統(tǒng)進(jìn)行數(shù)據(jù)拷貝,并且數(shù)據(jù)拷貝操作是由文件系統(tǒng)和硬件驅(qū)動(dòng)實(shí)現(xiàn)的,理論上來(lái)說(shuō),拷貝數(shù)據(jù)的效率是一樣的。
但是通過(guò)內(nèi)存映射的方法訪問(wèn)硬盤(pán)上的文件,效率要比read和write系統(tǒng)調(diào)用高,這是為什么?
read()是系統(tǒng)調(diào)用,首先將文件從硬盤(pán)拷貝到內(nèi)核空間的一個(gè)緩沖區(qū),再將這些數(shù)據(jù)拷貝到用戶空間,實(shí)際上進(jìn)行了兩次數(shù)據(jù)拷貝;
map()也是系統(tǒng)調(diào)用,但沒(méi)有進(jìn)行數(shù)據(jù)拷貝,當(dāng)缺頁(yè)中斷發(fā)生時(shí),直接將文件從硬盤(pán)拷貝到用戶空間,只進(jìn)行了一次數(shù)據(jù)拷貝。
所以,采用內(nèi)存映射的讀寫(xiě)效率要比傳統(tǒng)的read/write性能高。
拷貝視頻代碼舉例:
機(jī)器配置: 內(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(" 消耗時(shí)間:{} 秒",(System.currentTimeMillis()-start)/1000);
1.28G 大約消耗28秒時(shí)間
第二種方式:
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("拷貝文件消耗時(shí)間{}",(end-start)/1000);
同樣1.28G ,消耗時(shí)時(shí)間不到1秒? ?
但是 ,第二種方式,拷貝的視頻文件,不能播放,不知道什么因素,如果有知道解決方案的,麻煩給我留言一下或者email 80692072@qq.com? 謝謝。