Java 中的內(nèi)存映射 mmap
1、mmap 基礎(chǔ)概念
mmap
是一種內(nèi)存映射文件的方法,即將一個(gè)文件映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和一段進(jìn)程虛擬地址的映射。實(shí)現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會(huì)自動(dòng)回寫臟頁到對(duì)應(yīng)的文件磁盤上,即完成了對(duì)文件的操作而不必再調(diào)用 read
,write
等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對(duì)這段區(qū)域的修改也直接反映用戶空間,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。
mmap工作原理:
操作系統(tǒng)提供了這么一系列 mmap
的配套函數(shù)
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); int munmap( void * addr, size_t len); int msync( void *addr, size_t len, int flags);
2、Java 中的 mmap
Java
中原生讀寫方式大概可以被分為三種:普通 IO,FileChannel
(文件通道),mmap
(內(nèi)存映射)。區(qū)分他們也很簡單,例如 FileWriter
,FileReader
存在于 java.io
包中,他們屬于普通 IO;FileChannel
存在于 java.nio
包中,也是 Java 最常用的文件操作類;而今天的主角 mmap
,則是由 FileChannel
調(diào)用 map
方法衍生出來的一種特殊讀寫文件的方式,被稱之為內(nèi)存映射。
mmap 的使用方式:
FileChannel fileChannel = new RandomAccessFile(new File("db.data"), "rw").getChannel(); MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, filechannel.size();
MappedByteBuffer
便是 Java
中的 mmap
操作類。
// 寫 byte[] data = new byte[4]; int position = 8; // 從當(dāng)前 mmap 指針的位置寫入 4b 的數(shù)據(jù) mappedByteBuffer.put(data); // 指定 position 寫入 4b 的數(shù)據(jù) MappedByteBuffer subBuffer = mappedByteBuffer.slice(); subBuffer.position(position); subBuffer.put(data); // 讀 byte[] data = new byte[4]; int position = 8; // 從當(dāng)前 mmap 指針的位置讀取 4b 的數(shù)據(jù) mappedByteBuffer.get(data); // 指定 position 讀取 4b 的數(shù)據(jù) MappedByteBuffer subBuffer = mappedByteBuffer.slice(); subBuffer.position(position); subBuffer.get(data);
3、mmap 不是銀彈
促使我寫這一篇文章的一大動(dòng)力,來自于網(wǎng)絡(luò)中很多關(guān)于 mmap
錯(cuò)誤的認(rèn)知。初識(shí) mmap
,很多文章提到 mmap 適用于處理大文件的場景,現(xiàn)在回過頭看,其實(shí)這種觀點(diǎn)是非?;奶频模Mㄟ^此文能夠澄清 mmap 本來的面貌。
FileChannel
與 mmap
同時(shí)存在,大概率說明兩者都有其合適的使用場景,而事實(shí)也的確如此。在看待二者時(shí),可以將其看待成實(shí)現(xiàn)文件 IO
的兩種工具,工具本身沒有好壞,主要還是看使用場景。
4、mmap vs FileChannel
這一節(jié),詳細(xì)介紹一下 FileChannel
和 mmap
在進(jìn)行文件 IO 的一些異同點(diǎn)。
4.1 pageCache
FileChannel
和 mmap
的讀寫都經(jīng)過 pageCache
,或者更準(zhǔn)確的說法是通過 vmstat
觀測到的 cache 這一部分內(nèi)存,而非用戶空間的內(nèi)存。
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 0 0 4622324 40736 351384 0 0 0 0 2503 200 50 1 50 0 0
至于說 mmap
映射的這部分內(nèi)存能不能稱之為 pageCache
,我并沒有去調(diào)研過,不過在操作系統(tǒng)看來,他們并沒有太多的區(qū)別,這部分 cache
都是內(nèi)核在控制。后面本文也統(tǒng)一稱 mmap
出來的內(nèi)存為 pageCache
。
4.2 缺頁中斷
對(duì) Linux
文件 IO 有基礎(chǔ)認(rèn)識(shí)的讀者,可能對(duì)缺頁中斷這個(gè)概念也不會(huì)太陌生。mmap 和 FileChannel 都以缺頁中斷的方式,進(jìn)行文件讀寫。
以 mmap
讀取 1G 文件為例, fileChannel.map(FileChannel.MapMode.READ_WRITE
, 0, _GB); 進(jìn)行映射是一個(gè)消耗極少的操作,此時(shí)并不意味著 1G 的文件被讀進(jìn)了 pageCache
。只有通過以下方式,才能夠確保文件被讀進(jìn) pageCache
。
FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, _GB); for (int i = 0; i < _GB; i += _4kb) { temp += map.get(i); }
關(guān)于內(nèi)存對(duì)齊的細(xì)節(jié)在這里就不拓展了,可以詳見 java.nio.MappedByteBuffer#load
方法,load
方法也是通過按頁訪問的方式觸發(fā)中斷
如下是 pageCache
逐漸增長的過程,共計(jì)約增長了 1.034G
,說明文件內(nèi)容此刻已全部 load
。
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 4824640 1056 207912 0 0 0 0 2374 195 50 0 50 0 0
2 1 0 4605300 2676 411892 0 0 205256 0 3481 1759 52 2 34 12 0
2 1 0 4432560 2676 584308 0 0 172032 0 2655 346 50 1 25 24 0
2 1 0 4255080 2684 761104 0 0 176400 0 2754 380 50 1 19 29 0
2 3 0 4086528 2688 929420 0 0 167940 40 2699 327 50 1 25 24 0
2 2 0 3909232 2692 1106300 0 0 176520 4 2810 377 50 1 23 26 0
2 2 0 3736432 2692 1278856 0 0 172172 0 2980 361 50 1 17 31 0
3 0 0 3722064 2840 1292776 0 0 14036 0 2757 392 50 1 29 21 0
2 0 0 3721784 2840 1292892 0 0 116 0 2621 283 50 1 50 0 0
2 0 0 3721996 2840 1292892 0 0 0 0 2478 237 50 0 50 0 0
兩個(gè)細(xì)節(jié):
mmap
映射的過程可以理解為一個(gè)懶加載, 只有 get()
時(shí)才會(huì)觸發(fā)缺頁中斷
預(yù)讀大小是有操作系統(tǒng)算法決定的,可以默認(rèn)當(dāng)作 4kb
,即如果希望懶加載變成實(shí)時(shí)加載,需要按照 step=4kb
進(jìn)行一次遍歷
而 FileChannel
缺頁中斷的原理也與之相同,都需要借助 PageCache
做一層跳板,完成文件的讀寫。
4.3 內(nèi)存拷貝次數(shù)
很多言論認(rèn)為 mmap
相比 FileChannel
少一次復(fù)制,我個(gè)人覺得還是需要區(qū)分場景。
例如需求是從文件首地址讀取一個(gè) int,兩者所經(jīng)過的鏈路其實(shí)是一致的:SSD -> pageCache ->
應(yīng)用內(nèi)存,mmap
并不會(huì)少拷貝一次。
但如果需求是維護(hù)一個(gè) 100M 的復(fù)用 buffer
,且涉及到文件 IO,mmap
直接就可以當(dāng)做是 100M 的 buffer
來用,而不用在進(jìn)程的內(nèi)存(用戶空間)中再維護(hù)一個(gè) 100M 的緩沖。
4.4 用戶態(tài)與內(nèi)核態(tài)
用戶態(tài)和內(nèi)核態(tài):
操作系統(tǒng)出于安全考慮,將一些底層的能力進(jìn)行了封裝,提供了系統(tǒng)調(diào)用(system call
)給用戶使用。這里就涉及到“用戶態(tài)”和“內(nèi)核態(tài)”的切換問題,私認(rèn)為這里也是很多人概念理解模糊的重災(zāi)區(qū),我在此梳理下個(gè)人的認(rèn)知,如有錯(cuò)誤也歡迎指正。
先看 FileChannel
,下面兩段代碼,你認(rèn)為誰更快?
// 方法一: 4kb 刷盤 FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_4kb); for (int i = 0; i < _4kb; i++) { byteBuffer.put((byte)0); } for (int i = 0; i < _GB; i += _4kb) { byteBuffer.position(0); byteBuffer.limit(_4kb); fileChannel.write(byteBuffer); } // 方法二: 單字節(jié)刷盤 FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1); byteBuffer.put((byte)0); for (int i = 0; i < _GB; i ++) { byteBuffer.position(0); byteBuffer.limit(1); fileChannel.write(byteBuffer); }
使用方法一:4kb 緩沖刷盤(常規(guī)操作),在我的測試機(jī)器上只需要 1.2s 就寫完了 1G。而不使用任何緩沖的方法二,幾乎是直接卡死,文件增長速度非常緩慢,在等待了 5 分鐘還沒寫完后,中斷了測試。
使用寫入緩沖區(qū)是一個(gè)非常經(jīng)典的優(yōu)化技巧,用戶只需要設(shè)置 4kb 整數(shù)倍的寫入緩沖區(qū),聚合小數(shù)據(jù)的寫入,就可以使得數(shù)據(jù)從 pageCache
刷盤時(shí),盡可能是 4kb 的整數(shù)倍,避免寫入放大問題。但這不是這一節(jié)的重點(diǎn),大家有沒有想過,pageCache
其實(shí)本身也是一層緩沖,實(shí)際寫入 1byte
并不是同步刷盤的,相當(dāng)于寫入了內(nèi)存,pageCache
刷盤由操作系統(tǒng)自己決策。那為什么方法二這么慢呢? 主要就在于 filechannel
的 read/write
底層相關(guān)聯(lián)的系統(tǒng)調(diào)用,是需要切換內(nèi)核態(tài)和用戶態(tài)的,注意,這里跟內(nèi)存拷貝沒有任何關(guān)系,導(dǎo)致態(tài)切換的根本原因是 read/write
關(guān)聯(lián)的系統(tǒng)調(diào)用本身 。方法二比方法一多切換了 4096 倍,態(tài)的切換成為了瓶頸,導(dǎo)致耗時(shí)嚴(yán)重。
階段總結(jié)一下重點(diǎn),在 DRAM 中設(shè)置用戶寫入緩沖區(qū)這一行為有兩個(gè)意義:
- 方便做 4kb 對(duì)齊,ssd 刷盤友好
- 減少用戶態(tài)和內(nèi)核態(tài)的切換次數(shù),cpu 友好
但 mmap
不同,其底層提供的映射能力不涉及到切換內(nèi)核態(tài)和用戶態(tài),注意,這里跟內(nèi)存拷貝還是沒有任何關(guān)系,導(dǎo)致態(tài)不發(fā)生切換的根本原因是 mmap
關(guān)聯(lián)的系統(tǒng)調(diào)用本身。驗(yàn)證這一點(diǎn),也非常容易,我們使用 mmap 實(shí)現(xiàn)方法二來看看速度如何:
FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, _GB); for (int i = 0; i < _GB; i++) { map.put((byte)0); }
在我的測試機(jī)器上,花費(fèi)了 3s,它比 FileChannel + 4kb
緩沖寫要慢,但遠(yuǎn)比 FileChannel
寫單字節(jié)快。
這里也解釋了我之前文章《文件 IO 操作的一些最佳實(shí)踐》中一個(gè)疑問:"一次寫入很小量數(shù)據(jù)的場景使用 mmap
會(huì)比 fileChannel
快的多“,其背后的原理就和上述例子一樣,在小數(shù)據(jù)量下,瓶頸不在于 IO,而在于 用戶態(tài)和內(nèi)核態(tài)的切換 。
5、mmap 細(xì)節(jié)補(bǔ)充
5.1 copy on write 模式
我們注意到 public abstract MappedByteBuffer map(MapMode mode,long position, long size)
的第一個(gè)參數(shù),MapMode
其實(shí)有三個(gè)值,在網(wǎng)絡(luò)沖浪的時(shí)候,也幾乎沒有找到講解 MapMode
的文章。MapMode
有三個(gè)枚舉值 READ_WRITE
、 READ_ONLY
、 PRIVATE
,大多數(shù)時(shí)候使用的可能是 READ_WRITE
,而 READ_ONLY
不過是限制了 WRITE 而已,很容易理解,但這個(gè) PRIVATE
身上似乎有一層神秘的面紗。
實(shí)際上 PRIVATE
模式正是 mmap 的 copy on write
模式,當(dāng)使用 MapMode.PRIVATE
去映射文件時(shí),你會(huì)獲得以下的特性:
- 其他任何方式對(duì)文件的修改,會(huì)直接反映在當(dāng)前 mmap 映射中。
private mmap
之后自身的 put 行為,會(huì)觸發(fā)復(fù)制,形成自己的副本,任何修改不會(huì)會(huì)刷到文件中,也不再感知該文件該頁的改動(dòng)。
俗稱:copy on write
。
這有什么用呢?重點(diǎn)就在于任何修改都不會(huì)回刷文件。其一,你可以獲得一個(gè)文件副本,如果你正好有這個(gè)需求,直接可以使用 PRIVATE 模式去進(jìn)行映射,其二,令人有點(diǎn)小激動(dòng)的場景,你獲得了一塊真正的 PageCache
,不用擔(dān)心它會(huì)被操作系統(tǒng)刷盤造成 overhead
。假設(shè)你的機(jī)器配置如下:機(jī)器內(nèi)存 9G,JVM 參數(shù)設(shè)置為 6G,堆外限制為 2G,那剩下的 1G 只能被內(nèi)核態(tài)使用,如果想被用戶態(tài)的程序利用起來,就可以使用 mmap
的 copy on write
模式,這不會(huì)占用你的堆內(nèi)內(nèi)存或者堆外內(nèi)存。
5.2 回收 mmap 內(nèi)存
更正之前博文關(guān)于 mmap
內(nèi)存回收的一個(gè)錯(cuò)誤說法,回收 mmap
很簡單
((DirectBuffer) mmap).cleaner().clean();
mmap 的生命中簡單可以分為:map(映射),get/load
(缺頁中斷),clean
(回收)。一個(gè)實(shí)用的技巧是動(dòng)態(tài)分配的內(nèi)存映射區(qū)域,在讀取過后,可以異步回收掉。
6、mmap 使用場景
使用 mmap 處理小數(shù)據(jù)的頻繁讀寫
如果 IO 非常頻繁,數(shù)據(jù)卻非常小,推薦使用 mmap
,以避免 FileChannel
導(dǎo)致的切態(tài)問題。例如索引文件的追加寫。
6.1 mmap 緩存
當(dāng)使用 FileChannel
進(jìn)行文件讀寫時(shí),往往需要一塊寫入緩存以達(dá)到聚合的目的,最常使用的是堆內(nèi)/堆外內(nèi)存,但他們都有一個(gè)問題,即當(dāng)進(jìn)程掛掉后,堆內(nèi)/堆外內(nèi)存會(huì)立刻丟失,這一部分沒有落盤的數(shù)據(jù)也就丟了。而使用 mmap
作為緩存,會(huì)直接存儲(chǔ)在 pageCache
中,不會(huì)導(dǎo)致數(shù)據(jù)丟失,盡管這只能規(guī)避進(jìn)程被 kill 這種情況,無法規(guī)避掉電。
6.2 小文件的讀寫
恰恰和網(wǎng)傳的很多言論相反,mmap 由于其不切態(tài)的特性,特別適合順序讀寫,但由于 sun.nio.ch.FileChannelImpl#map(MapMode mode, long position, long size)
中 size 的限制,只能傳遞一個(gè) int 值,所以,單次 map 單個(gè)文件的長度不能超過 2G,如果將 2G 作為文件大 or 小的閾值,那么小于 2G 的文件使用 mmap 來讀寫一般來說是有優(yōu)勢的。在 RocketMQ
中也利用了這一點(diǎn),為了能夠方便的使用 mmap,將 commitLog
的大小按照 1G 來進(jìn)行切分。對(duì)的,忘記說了,RocketMQ
等消息隊(duì)列一直在使用 mmap。
6.3 cpu 緊俏下的讀寫
在大多數(shù)場景下,FileChannel
和讀寫緩沖的組合相比 mmap 要占據(jù)優(yōu)勢,或者說不分伯仲,但在 cpu 緊俏下的讀寫,使用 mmap 進(jìn)行讀寫往往能起到優(yōu)化的效果,它的根據(jù)是 mmap
不會(huì)出現(xiàn)用戶態(tài)和內(nèi)核態(tài)的切換,導(dǎo)致 cpu 的不堪重負(fù)(但這樣承擔(dān)起動(dòng)態(tài)映射與異步回收內(nèi)存的開銷)。
6.4 特殊軟硬件因素
例如持久化內(nèi)存 Pmem
、不同代數(shù)的 SSD、不同主頻的 CPU、不同核數(shù)的 CPU、不同的文件系統(tǒng)、文件系統(tǒng)的掛載方式...等等因素都會(huì)影響 mmap
和 filechannel read/write
的快慢,因?yàn)樗麄儗?duì)應(yīng)的系統(tǒng)調(diào)用是不同的。只有 benchmark
過后,方知快慢。
到此這篇關(guān)于Java 中的內(nèi)存映射 mmap的文章就介紹到這了,更多相關(guān)Java 中的內(nèi)存映射內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring aop實(shí)現(xiàn)接口超時(shí)處理組件的代碼詳解
這篇文章給大家介紹了spring aop實(shí)現(xiàn)接口超時(shí)處理組件,文中有詳細(xì)的實(shí)現(xiàn)思路,并通過代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-02-02Java中一維二維數(shù)組的靜態(tài)和動(dòng)態(tài)初始化
今天通過本文給大家分享Java中的數(shù)組,包括一維數(shù)組和二維數(shù)組的靜態(tài)初始化和動(dòng)態(tài)初始化問題,感興趣的朋友一起看看吧2017-10-10IntelliJ IDEA編譯項(xiàng)目報(bào)錯(cuò) "xxx包不存在" 或 "找不到符號(hào)"
這篇文章主要介紹了IntelliJ IDEA編譯項(xiàng)目報(bào)錯(cuò) "xxx包不存在" 或 "找不到符號(hào)" ,文中通過圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08Nacos客戶端配置中心緩存動(dòng)態(tài)更新實(shí)現(xiàn)源碼
這篇文章主要為大家介紹了Nacos客戶端配置中心緩存動(dòng)態(tài)更新實(shí)現(xiàn)源碼,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-03-03RecyclerChart動(dòng)態(tài)屬性圖標(biāo)聯(lián)動(dòng)數(shù)據(jù)動(dòng)態(tài)加載詳解
這篇文章主要為大家介紹了RecyclerChart動(dòng)態(tài)屬性圖標(biāo)聯(lián)動(dòng)數(shù)據(jù)動(dòng)態(tài)加載詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03關(guān)于如何正確地定義Java內(nèi)部類方法詳解
在Java中,我們通常是把不同的類創(chuàng)建在不同的包里面,對(duì)于同一個(gè)包里的類來說,它們都是同一層次的,但其實(shí)還有另一種情況,有些類可以被定義在另一個(gè)類的內(nèi)部,本文將詳細(xì)帶你了解如何正確地定義Java內(nèi)部類,需要的朋友可以參考下2023-05-05java編寫創(chuàng)建數(shù)據(jù)庫和表的程序
這篇文章主要為大家詳細(xì)介紹了java編寫創(chuàng)建數(shù)據(jù)庫和表的程序,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10Java的Lambda表達(dá)式和Stream流的作用以及示例
這篇文章主要介紹了Java的Lambda表達(dá)式和Stream流簡單示例,Lambda允許把函數(shù)作為一個(gè)方法的參數(shù),使用Lambda表達(dá)式可以寫出更簡潔、更靈活的代碼,而其作為一種更緊湊的代碼風(fēng)格,使Java的語言表達(dá)能力得到了提升,需要的朋友可以參考下2023-05-05