深入淺出MappedByteBuffer(推薦)
java io操作中通常采用BufferedReader,BufferedInputStream等帶緩沖的IO類處理大文件,不過java nio中引入了一種基于MappedByteBuffer操作大文件的方式,其讀寫性能極高,本文會介紹其性能如此高的內(nèi)部實(shí)現(xiàn)原理。
在深入MappedByteBuffer之前,先看看計算機(jī)內(nèi)存管理的一些知識:
1.內(nèi)存管理
- MMC:CPU的內(nèi)存管理單元。
- 物理內(nèi)存:即內(nèi)存條的內(nèi)存空間。
- 虛擬內(nèi)存:計算機(jī)系統(tǒng)內(nèi)存管理的一種技術(shù)。它使得應(yīng)用程序認(rèn)為它擁有連續(xù)可用的內(nèi)存(一個連續(xù)完整的地址空間),而實(shí)際上,它通常是被分隔成多個物理內(nèi)存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進(jìn)行數(shù)據(jù)交換。
- 頁面文件:操作系統(tǒng)反映構(gòu)建并使用虛擬內(nèi)存的硬盤空間大小而創(chuàng)建的文件,在windows下,即pagefile.sys文件,其存在意味著物理內(nèi)存被占滿后,將暫時不用的數(shù)據(jù)移動到硬盤上。
- 缺頁中斷:當(dāng)程序試圖訪問已映射在虛擬地址空間中但未被加載至物理內(nèi)存的一個分頁時,由MMC發(fā)出的中斷。如果操作系統(tǒng)判斷此次訪問是有效的,則嘗試將相關(guān)的頁從虛擬內(nèi)存文件中載入物理內(nèi)存。
那么問題來了,為什么會有虛擬內(nèi)存和物理內(nèi)存的區(qū)別?
如果正在運(yùn)行的一個進(jìn)程,它所需的內(nèi)存是有可能大于內(nèi)存條容量之和的,如內(nèi)存條是256M,程序卻要創(chuàng)建一個2G的數(shù)據(jù)區(qū),那么所有數(shù)據(jù)不可能都加載到內(nèi)存(物理內(nèi)存),必然有數(shù)據(jù)要放到其他介質(zhì)中(比如硬盤),待進(jìn)程需要訪問那部分?jǐn)?shù)據(jù)時,再調(diào)度進(jìn)入物理內(nèi)存。
什么是虛擬內(nèi)存地址和物理內(nèi)存地址?
假設(shè)你的計算機(jī)是32位,那么它的地址總線是32位的,也就是它可以尋址00xFFFFFFFF(4G)的地址空間,但如果你的計算機(jī)只有256M的物理內(nèi)存0x0x0FFFFFFF(256M),同時你的進(jìn)程產(chǎn)生了一個不在這256M地址空間中的地址,那么計算機(jī)該如何處理呢?回答這個問題前,先說明計算機(jī)的內(nèi)存分頁機(jī)制
計算機(jī)會對虛擬內(nèi)存地址空間(32位為4G)進(jìn)行分頁產(chǎn)生頁(page),對物理內(nèi)存地址空間(假設(shè)256M)進(jìn)行分頁產(chǎn)生頁幀(page frame),頁和頁幀的大小一樣,所以虛擬內(nèi)存頁的個數(shù)勢必要大于物理內(nèi)存頁幀的個數(shù)。
在計算機(jī)上有一個頁表(page table),就是映射虛擬內(nèi)存頁到物理內(nèi)存頁的,更確切的說是頁號到頁幀號的映射,而且是一對一的映射。
問題來了,虛擬內(nèi)存頁的個數(shù) > 物理內(nèi)存頁幀的個數(shù),豈不是有些虛擬內(nèi)存頁的地址永遠(yuǎn)沒有對應(yīng)的物理內(nèi)存地址空間?
答案肯定是否定的,操作系統(tǒng)是這樣處理的。
操作系統(tǒng)有個頁面失效(page fault)功能。操作系統(tǒng)找到一個最少使用的頁幀,使之失效,并把它寫入磁盤,隨后把需要訪問的頁放到頁幀中,并修改頁表中的映射,保證了所有的頁都會被調(diào)度。
我們簡單總結(jié)一下上面的內(nèi)容
虛擬內(nèi)存地址:由頁號(與頁表中的頁號關(guān)聯(lián))和偏移量(頁的小大,即這個頁能存多少數(shù)據(jù))組成。
舉個例子,有一個虛擬地址它的頁號是4,偏移量是20,那么他的尋址過程是這樣的:
首先到頁表中找到頁號4對應(yīng)的頁幀號(比如為8),如果頁不在內(nèi)存中,則用失效機(jī)制調(diào)入頁,接著把頁幀號和偏移量傳給MMC組成一個物理上真正存在的地址,最后就是訪問物理內(nèi)存的數(shù)據(jù)了。
2.MappedByteBuffer的深度剖析
從繼承結(jié)構(gòu)上看,MappedByteBuffer繼承自ByteBuffer,內(nèi)部維護(hù)了一個邏輯地址address。
我們通過一個簡單的示例看看MappedByBuffer是怎么使用的
import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.util.Scanner; public class MappedByteBufferTest { public static void main(String[] args) { File file = new File("D://data.txt"); long len = file.length(); byte[] ds = new byte[(int) len]; try { MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r") .getChannel() .map(FileChannel.MapMode.READ_ONLY, 0, len); for (int offset = 0; offset < len; offset++) { byte b = mappedByteBuffer.get(); ds[offset] = b; } Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" "); while (scan.hasNext()) { System.out.print(scan.next() + " "); } } catch (IOException e) { } } }
上面代碼是不是看的不是很明白,我逐一給大家解釋一下
整個代碼其實(shí)就包含了兩個過程:
- map過程
- get過程
2.1 map過程
FileChannel提供了map方法把文件映射到虛擬內(nèi)存,通常情況可以映射整個文件,如果文件比較大,可以進(jìn)行分段映射。
FileChannel中的幾個變量:
- MapMode mode:內(nèi)存映像文件訪問的方式,共三種
- MapMode.READ_ONLY:只讀,試圖修改得到的緩沖區(qū)將導(dǎo)致拋出異常。
- MapMode.READ_WRITE:讀/寫,對得到的緩沖區(qū)的更改最終將寫入文件;但該更改對映射到同一文件的其他程序不一定是可見的。
- MapMode.PRIVATE:私用,可讀可寫,但是修改的內(nèi)容不會寫入文件,只是buffer自身的改變,這種能力稱之為”copy on write”。
- position:文件映射時的起始位置
- allocationGranularity::Memory allocation size for mapping buffers,通過native函數(shù)initIDs初始化。、
因?yàn)镸appedByteBuffer做的實(shí)在是太過于優(yōu)秀了,所以我不得不帶著大家讀一下源碼,看看他們的設(shè)計思想和思路
1.通過RandomAccessFile獲取FileChannel
public final FileChannel getChannel() { synchronized (this) { if (channel == null) { channel = FileChannelImpl.open(fd, path, true, rw, this); } return channel; } }
上述實(shí)現(xiàn)可以看出,由于synchronized ,只有一個線程能夠初始化FileChannel。
2.通過FileChannel.map方法,把文件映射到虛擬內(nèi)存,并返回邏輯地址address,實(shí)現(xiàn)如下:
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { int pagePosition = (int)(position % allocationGranularity); long mapPosition = position - pagePosition; long mapSize = size + pagePosition; try { addr = map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError x) { System.gc(); try { Thread.sleep(100); } catch (InterruptedException y) { Thread.currentThread().interrupt(); } try { addr = map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError y) { // After a second OOME, fail throw new IOException("Map failed", y); } } int isize = (int)size; Unmapper um = new Unmapper(addr, mapSize, isize, mfd); if ((!writable) || (imode == MAP_RO)) { return Util.newMappedByteBufferR(isize, addr + pagePosition, mfd, um); } else { return Util.newMappedByteBuffer(isize, addr + pagePosition, mfd, um); } }
上述代碼可以看出,最終map通過native函數(shù)map0完成文件的映射工作。
- 如果第一次文件映射導(dǎo)致OOM,則手動觸發(fā)垃圾回收,休眠100ms后再次嘗試映射,如果失敗,則拋出異常。
- 通過newMappedByteBuffer方法初始化MappedByteBuffer實(shí)例,不過其最終返回的是DirectByteBuffer的實(shí)例,實(shí)現(xiàn)如下:
static MappedByteBuffer newMappedByteBuffer(int size, long addr, FileDescriptor fd, Runnable unmapper) { MappedByteBuffer dbb; if (directByteBufferConstructor == null) initDBBConstructor(); dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance( new Object[] { new Integer(size), new Long(addr), fd, unmapper } return dbb; } // 訪問權(quán)限 private static void initDBBConstructor() { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { Class<?> cl = Class.forName("java.nio.DirectByteBuffer"); Constructor<?> ctor = cl.getDeclaredConstructor( new Class<?>[] { int.class, long.class, FileDescriptor.class, Runnable.class }); ctor.setAccessible(true); directByteBufferConstructor = ctor; }}); }
由于FileChannelImpl和DirectByteBuffer不在同一個包中,所以有權(quán)限訪問問題,通過AccessController類獲取DirectByteBuffer的構(gòu)造器進(jìn)行實(shí)例化。
DirectByteBuffer是MappedByteBuffer的一個子類,其實(shí)現(xiàn)了對內(nèi)存的直接操作。
2.2 get過程
MappedByteBuffer的get方法最終通過DirectByteBuffer.get方法實(shí)現(xiàn)的。
public byte get() { return ((unsafe.getByte(ix(nextGetIndex())))); } public byte get(int i) { return ((unsafe.getByte(ix(checkIndex(i))))); } private long ix(int i) { return address + (i << 0); }
map0()函數(shù)返回一個地址address,這樣就無需調(diào)用read或write方法對文件進(jìn)行讀寫,通過address就能夠操作文件。
底層采用unsafe.getByte方法,通過(address + 偏移量)獲取指定內(nèi)存的數(shù)據(jù)。
第一次訪問address所指向的內(nèi)存區(qū)域,導(dǎo)致缺頁中斷,中斷響應(yīng)函數(shù)會在交換區(qū)中查找相對應(yīng)的頁面,如果找不到(也就是該文件從來沒有被讀入內(nèi)存的情況),則從硬盤上將文件指定頁讀取到物理內(nèi)存中(非jvm堆內(nèi)存)。
如果在拷貝數(shù)據(jù)時,發(fā)現(xiàn)物理內(nèi)存不夠用,則會通過虛擬內(nèi)存機(jī)制(swap)將暫時不用的物理頁面交換到硬盤的虛擬內(nèi)存中。
3.性能分析
從代碼層面上看,從硬盤上將文件讀入內(nèi)存,都要經(jīng)過文件系統(tǒng)進(jìn)行數(shù)據(jù)拷貝,并且數(shù)據(jù)拷貝操作是由文件系統(tǒng)和硬件驅(qū)動實(shí)現(xiàn)的,理論上來說,拷貝數(shù)據(jù)的效率是一樣的。
但是通過內(nèi)存映射的方法訪問硬盤上的文件,效率要比read和write系統(tǒng)調(diào)用高,這是為什么?
read()是系統(tǒng)調(diào)用,首先將文件從硬盤拷貝到內(nèi)核空間的一個緩沖區(qū),再將這些數(shù)據(jù)拷貝到用戶空間,實(shí)際上進(jìn)行了兩次數(shù)據(jù)拷貝;
map()也是系統(tǒng)調(diào)用,但沒有進(jìn)行數(shù)據(jù)拷貝,當(dāng)缺頁中斷發(fā)生時,直接將文件從硬盤拷貝到用戶空間,只進(jìn)行了一次數(shù)據(jù)拷貝。
所以,采用內(nèi)存映射的讀寫效率要比傳統(tǒng)的read/write性能高。
4.總結(jié)
MappedByteBuffer使用虛擬內(nèi)存,因此分配(map)的內(nèi)存大小不受JVM的-Xmx參數(shù)限制,但是也是有大小限制的。
如果當(dāng)文件超出1.5G限制時,可以通過position參數(shù)重新map文件后面的內(nèi)容。
MappedByteBuffer在處理大文件時的確性能很高,但也存在一些問題,如:內(nèi)存占用、文件關(guān)閉不確定,被其打開的文件只有在垃圾回收的才會被關(guān)閉,而且這個時間點(diǎn)是不確定的。
javadoc中也提到:
A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected.*
翻譯過來便是:
映射的字節(jié)緩沖區(qū)及其表示的文件映射在緩沖區(qū)本身被垃圾收集之前保持有效
到此這篇關(guān)于深入淺出MappedByteBuffer的文章就介紹到這了,更多相關(guān)MappedByteBuffer簡介內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringAop切入點(diǎn)execution表達(dá)式的深入講解
Spring AOP 可能會經(jīng)常使用 execution切入點(diǎn)指示符,下面這篇文章主要給大家介紹了關(guān)于SpringAop切入點(diǎn)execution表達(dá)式的相關(guān)資料,需要的朋友可以參考下2021-08-08Java RPC框架如何實(shí)現(xiàn)客戶端限流配置
這篇文章主要介紹了Java RPC框架如何實(shí)現(xiàn)客戶端限流配置,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-02-02淺析Android系統(tǒng)中HTTPS通信的實(shí)現(xiàn)
這篇文章主要介紹了淺析Android系統(tǒng)中HTTPS通信的實(shí)現(xiàn),實(shí)現(xiàn)握手的源碼為Java語言編寫,需要的朋友可以參考下2015-07-07java8 利用reduce實(shí)現(xiàn)將列表中的多個元素的屬性求和并返回操作
這篇文章主要介紹了java8 利用reduce實(shí)現(xiàn)將列表中的多個元素的屬性求和并返回操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08springboot項(xiàng)目集成swagger-bootstrap-ui全過程
這篇文章主要介紹了springboot項(xiàng)目集成swagger-bootstrap-ui全過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05