自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

重新認(rèn)識(shí) Java 中的內(nèi)存映射(Mmap)

開發(fā) 后端 存儲(chǔ)軟件
mmap 是一種內(nèi)存映射文件的方法,即將一個(gè)文件映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和一段進(jìn)程虛擬地址的映射。

[[434443]]

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)回寫臟頁到對應(yīng)的文件磁盤上,即完成了對文件的操作而不必再調(diào)用 read,write 等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。

mmap工作原理

操作系統(tǒng)提供了這么一系列 mmap 的配套函數(shù)。

  1. void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); 
  2. int munmap( void * addr, size_t len); 
  3. int msync( void *addr, size_t len, int flags); 

Java 中的 mmap

Java 中原生讀寫方式大概可以被分為三種:普通 IO,F(xiàn)ileChannel(文件通道),mmap(內(nèi)存映射)。區(qū)分他們也很簡單,例如 FileWriter,FileReader 存在于 java.io 包中,他們屬于普通 IO;FileChannel 存在于 java.nio 包中,也是 Java 最常用的文件操作類;而今天的主角 mmap,則是由 FileChannel 調(diào)用 map 方法衍生出來的一種特殊讀寫文件的方式,被稱之為內(nèi)存映射。

mmap 的使用方式:

  1. FileChannel fileChannel = new RandomAccessFile(new File("db.data"), "rw").getChannel(); 
  2. MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, filechannel.size(); 

MappedByteBuffer 便是 Java 中的 mmap 操作類。

  1. // 寫 
  2. byte[] data = new byte[4]; 
  3. int position = 8; 
  4. // 從當(dāng)前 mmap 指針的位置寫入 4b 的數(shù)據(jù) 
  5. mappedByteBuffer.put(data); 
  6. // 指定 position 寫入 4b 的數(shù)據(jù) 
  7. MappedByteBuffer subBuffer = mappedByteBuffer.slice(); 
  8. subBuffer.position(position); 
  9. subBuffer.put(data); 
  10.  
  11. // 讀 
  12. byte[] data = new byte[4]; 
  13. int position = 8; 
  14. // 從當(dāng)前 mmap 指針的位置讀取 4b 的數(shù)據(jù) 
  15. mappedByteBuffer.get(data); 
  16. // 指定 position 讀取 4b 的數(shù)據(jù) 
  17. MappedByteBuffer subBuffer = mappedByteBuffer.slice(); 
  18. subBuffer.position(position); 
  19. subBuffer.get(data); 

mmap 不是銀彈

促使我寫這一篇文章的一大動(dòng)力,來自于網(wǎng)絡(luò)中很多關(guān)于 mmap 錯(cuò)誤的認(rèn)知。初識(shí) mmap,很多文章提到 mmap 適用于處理大文件的場景,現(xiàn)在回過頭看,其實(shí)這種觀點(diǎn)是非?;奶频?,希望通過此文能夠澄清 mmap 本來的面貌。

FileChannel 與 mmap 同時(shí)存在,大概率說明兩者都有其合適的使用場景,而事實(shí)也的確如此。在看待二者時(shí),可以將其看待成實(shí)現(xiàn)文件 IO 的兩種工具,工具本身沒有好壞,主要還是看使用場景。

mmap vs FileChannel

這一節(jié),詳細(xì)介紹一下 FileChannel 和 mmap 在進(jìn)行文件 IO 的一些異同點(diǎn)。

pageCache

FileChannel 和 mmap 的讀寫都經(jīng)過 pageCache,或者更準(zhǔn)確的說法是通過 vmstat 觀測到的 cache 這一部分內(nèi)存,而非用戶空間的內(nèi)存。

  1. procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- 
  2.  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st 
  3.  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。

缺頁中斷

對 Linux 文件 IO 有基礎(chǔ)認(rèn)識(shí)的讀者,可能對缺頁中斷這個(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。

  1. FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); 
  2. MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, _GB); 
  3. for (int i = 0; i < _GB; i += _4kb) { 
  4.  temp += map.get(i); 

關(guān)于內(nèi)存對齊的細(xì)節(jié)在這里就不拓展了,可以詳見 java.nio.MappedByteBuffer#load 方法,load 方法也是通過按頁訪問的方式觸發(fā)中斷

如下是 pageCache 逐漸增長的過程,共計(jì)約增長了 1.034G,說明文件內(nèi)容此刻已全部 load。

  1. procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- 
  2.  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st 
  3.  2  0      0 4824640   1056 207912    0    0     0     0 2374  195 50  0 50  0  0 
  4.  2  1      0 4605300   2676 411892    0    0 205256     0 3481 1759 52  2 34 12  0 
  5.  2  1      0 4432560   2676 584308    0    0 172032     0 2655  346 50  1 25 24  0 
  6.  2  1      0 4255080   2684 761104    0    0 176400     0 2754  380 50  1 19 29  0 
  7.  2  3      0 4086528   2688 929420    0    0 167940    40 2699  327 50  1 25 24  0 
  8.  2  2      0 3909232   2692 1106300    0    0 176520     4 2810  377 50  1 23 26  0 
  9.  2  2      0 3736432   2692 1278856    0    0 172172     0 2980  361 50  1 17 31  0 
  10.  3  0      0 3722064   2840 1292776    0    0 14036     0 2757  392 50  1 29 21  0 
  11.  2  0      0 3721784   2840 1292892    0    0   116     0 2621  283 50  1 50  0  0 
  12.  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 做一層跳板,完成文件的讀寫。

內(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 的緩沖。

用戶態(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)為誰更快?

  1. // 方法一: 4kb 刷盤 
  2. FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); 
  3. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_4kb); 
  4. for (int i = 0; i < _4kb; i++) { 
  5.     byteBuffer.put((byte)0); 
  6. for (int i = 0; i < _GB; i += _4kb) { 
  7.     byteBuffer.position(0); 
  8.     byteBuffer.limit(_4kb); 
  9.     fileChannel.write(byteBuffer); 
  10.  
  11. // 方法二: 單字節(jié)刷盤 
  12. FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); 
  13. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1); 
  14. byteBuffer.put((byte)0); 
  15. for (int i = 0; i < _GB; i ++) { 
  16.     byteBuffer.position(0); 
  17.     byteBuffer.limit(1); 
  18.     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 對齊,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)方法二來看看速度如何:

  1. FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); 
  2. MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, _GB); 
  3. for (int i = 0; i < _GB; i++) { 
  4.   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)的切換。

mmap 細(xì)節(jié)補(bǔ)充

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ì)獲得以下的特性:

其他任何方式對文件的修改,會(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)存。

回收 mmap 內(nèi)存

更正之前博文關(guān)于 mmap 內(nèi)存回收的一個(gè)錯(cuò)誤說法,回收 mmap 很簡單

  1. ((DirectBuffer) mmap).cleaner().clean(); 

mmap 的生命中簡單可以分為:map(映射),get/load (缺頁中斷),clean(回收)。一個(gè)實(shí)用的技巧是動(dòng)態(tài)分配的內(nèi)存映射區(qū)域,在讀取過后,可以異步回收掉。

mmap 使用場景

使用 mmap 處理小數(shù)據(jù)的頻繁讀寫

如果 IO 非常頻繁,數(shù)據(jù)卻非常小,推薦使用 mmap,以避免 FileChannel 導(dǎo)致的切態(tài)問題。例如索引文件的追加寫。

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ī)避掉電。

小文件的讀寫

恰恰和網(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)行切分。對的,忘記說了,RocketMQ 等消息隊(duì)列一直在使用 mmap。

cpu 緊俏下的讀寫

在大多數(shù)場景下,F(xiàn)ileChannel 和讀寫緩沖的組合相比 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)存的開銷)。

特殊軟硬件因素

例如持久化內(nèi)存 Pmem、不同代數(shù)的 SSD、不同主頻的 CPU、不同核數(shù)的 CPU、不同的文件系統(tǒng)、文件系統(tǒng)的掛載方式...等等因素都會(huì)影響 mmap 和 filechannel read/write 的快慢,因?yàn)樗麄儗?yīng)的系統(tǒng)調(diào)用是不同的。只有 benchmark 過后,方知快慢。

 

責(zé)任編輯:武曉燕 來源: Kirito的技術(shù)分享
相關(guān)推薦

2014-01-06 11:23:54

Mesos設(shè)計(jì)架構(gòu)

2016-12-13 15:41:40

JavaHashMap

2021-04-22 21:15:38

Generator函數(shù)生成器

2019-02-24 21:27:26

物聯(lián)網(wǎng)網(wǎng)關(guān)物聯(lián)網(wǎng)IOT

2019-10-31 13:40:52

JavaPHP編程語言

2016-11-07 11:34:28

數(shù)據(jù)可視化大數(shù)據(jù)

2023-03-01 10:37:51

2017-01-03 17:22:16

公共云安全

2020-09-17 07:08:04

TypescriptVue3前端

2019-09-02 08:53:46

程序員

2015-03-19 10:15:54

程序員價(jià)值程序員價(jià)值

2012-01-11 09:12:25

程序員

2009-11-26 16:57:09

Cisco路由器ARP

2010-02-25 09:57:35

2012-06-26 11:11:44

架構(gòu)師

2011-04-25 17:15:39

MongodbMMAP

2023-05-03 09:09:28

Golang數(shù)組

2018-08-20 16:25:48

編程語言Java異常處理

2014-06-16 10:02:42

SwiftiOSWWDC

2010-10-22 11:10:24

軟考
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)