MappedByteBuffer

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? 謝謝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容