mmapmmap是一种内存映射文件的方法,即将一个文件映射到一个进程的地址空间,实现文件磁盘地址之间的映射和一个进程虚拟地址。实现了这样的映射关系后,进程就可以使用指针对这段内存进行读写,系统会自动将脏页回写到对应的文件盘中,即不调用read就完成了对文件的操作,write等系统调用函数。相反,内核空间对这个区域的修改也直接反映到用户空间,这样就可以实现不同进程之间的文件共享。mmap的工作原理操作系统提供了mmap这样一系列的支持功能。void*mmap(void*start,size_tlength,intprot,intflags,intfd,off_toffset);intmunmap(void*addr,size_tlen);intmsync(void*addr,size_tlen,intflags);Java中的mmap分为普通IO、FileChannel(文件通道)、mmap(内存映射)三种。区分它们也很简单。比如FileWriter和FileReader存在于java.io包中,属于普通IO;FileChannel存在于java.nio包中,也是Java中最常用的文件操作类;而今天的主角mmap,是由调用FileChannel的map方法衍生出来的一种特殊的文件读写方式,称为内存映射。mmap的使用方法:FileChannelfileChannel=newRandomAccessFile(newFile("db.data"),"rw").getChannel();MappedByteBuffermappedByteBuffer=fileChannel.map(FileChannel.MapMode.READ_WRITE,0,filechannel.size();MappedByteBuffer是Java中的mmap操作类。//Writebyte[]data=newbyte[4];intposition=8;//从当前mmap指针位置开始写4b数据mappedByteBuffer.put(data);//在指定位置写4b数据MappedByteBuffersubBuffer=mappedByteBuffer.slice();subBuffer.position(position);subBuffer.put(data);//读取byte[]data=newbyte[4];intposition=8;//从当前mmap指针的位置读取4b数据mappedByteBuffer。get(data);//指定读取4b数据的位置MappedByteBuffersubBuffer=mappedByteBuffer.slice();subBuffer.position(position);subBuffer.get(data);mmap不是促使我写这篇文章的灵丹妙药动机源于网络上很多对mmap的错误认知。刚认识mmap的时候,很多文章都提到mmap适合处理大文件。现在回想起来,这种看法其实是非常荒谬的。希望这篇文章能够厘清mmap的本来面目。FileChannel和mmap同时存在,意味着两者都有适合的使用场景,事实也确实如此。看两者的时候,可以看成是实现文件IO的两个工具。工具本身没有好坏之分,主要看使用场景。mmapvsFileChannel这一节详细介绍了FileChannel和mmap在文件IO上的一些异同。pageCacheFileChannel和mmap都是通过pageCache读写的,或者更准确的说是通过vmstat观察到的cache内存的一部分,并不是用户空间的内存。procs------------memory------------swap--------io-----system--------cpu-----rbswpdfreebuffcachesisobiboincsussyidwast300462232440736351384000025032005015000至于mmap映射的这部分内存是否可以称为pageCache,我没有研究过,但是从操作系统的角度来看,它们之间没有太大区别,这部分缓存在内核控制。在本文后面,来自mmap的内存也统称为pageCache。对Linux文件IO有基本了解的读者可能对缺页中断的概念不会太陌生。mmap和FileChannel都是以缺页中断的形式读写文件。以mmap读取1G文件为例,fileChannel.map(FileChannel.MapMode.READ_WRITE,0,_GB);映射是一个消耗很小的操作,并不代表1G的文件已经读入pageCache。只有通过以下方式才能保证文件被读入pageCache。FileChannelfileChannel=newRandomAccessFile(file,"rw").getChannel();MappedByteBuffermap=fileChannel.map(MapMode.READ_WRITE,0,_GB);for(inti=0;i<_GB;i+=_4kb){temp+=map.get(i);}关于内存对齐的细节这里就不展开了。具体可以参考java.nio.MappedByteBuffer#load方法。load方法也是通过逐页访问触发中断。下面是pageCache逐渐增长的过程,一共大约增加了1.034G,说明此时文件内容已经加载完毕。procs-----------memory-----------swap-------io-----system--------cpu-----rbswpdfreebuffcachesisobiboincsussyidwast2004824640105620791200002374195500500021046053002676411892002052560348117595223412021044325602676584308001720320265534650125240210425508026847611040017640002754380501192902304086528268892942000167940402699327501252402203909232269211063000017652042810377501232602203736432269212788560017217202980361501173103003722064284012927760014036027573925012921020037217842840129289200116026212835015000200372199628401292892000024782375005000两个细节:mmap映射的过程可以理解为一个懒加载,只有get()时才会触发缺页中断预读大小是有操作系统算法决定的,可以默认当For4kb,thatis,ifyouwantlazyloadingtobecomereal-timeloading,youneedtoperformatraversalaccordingtostep=4kb.TheprincipleofFileChannelpagefaultinterruptionisthesame,andyouneedtousePageCacheasaspringboardtocompletefilereadingandwriting.ManypeoplesaythatthenumberofmemorycopiesisthatmmaphasonelesscopythanFileChannel.Personally,Ithinkitisstillnecessarytodistinguishthescene.Forexample,therequirementistoreadanintfromthefirstaddressofthefile.Thelinkspassedbythetwoareactuallythesame:SSD->pageCache->applicationmemory,andmmapwillnotmakeonelesscopy.Butiftherequirementistomaintaina100MmultiplexingbufferandfileIOisinvolved,mmapcanbeuseddirectlyasa100Mbufferinsteadofmaintaininga100Mbufferintheprocessmemory(userspace).用户态和内核态用户态和内核态操作系统出于安全原因封装了一些底层能力,并提供系统调用(systemcalls)供用户使用。这涉及在“用户模式”和“内核模式”之间切换。我个人认为,这也是重灾区,很多人的概念认识都比较模糊。我在这里梳理一下个人的认知。如有错误,请指正。先看FileChannel,下面两段代码,你觉得哪一段更快?//方法一:4kb闪存盘FileChannelfileChannel=newRandomAccessFile(file,"rw").getChannel();ByteBufferbyteBuffer=ByteBuffer.allocateDirect(_4kb);for(inti=0;i<_4kb;i++){byteBuffer.put((byte)0);}for(inti=0;i<_GB;i+=_4kb){byteBuffer.position(0);byteBuffer。限制(_4kb);fileChannel.write(byteBuffer);}//方法二:单字节刷入FileChannelfileChannel=newRandomAccessFile(file,"rw").getChannel();ByteBufferbyteBuffer=ByteBuffer.allocateDirect(1);byteBuffer.put((byte)0);for(inti=0;i<_GB;i++){byteBuffer.position(0);byteBuffer.limit(1);fileChannel.write(byteBuffer);}使用方法一:4kb缓冲闪存盘(正常运行),它在我的测试机上只需要1.2s就可以写入1G。第二种方法没有使用任何缓冲几乎直接卡死,文件增长速度很慢,等了5分钟没有写完就中断测试了。使用写缓冲区是一个非常经典的优化技术。用户只需要设置一个4kb整数倍的writebuffer,聚合小数据的写入,这样数据从pageCacheflush的时候,可以尽可能的4kb的整数倍。,避免写放大问题。但这不是本节的重点。你有没有想过pageCache本身其实就是一层缓冲。实际写入1个字节不是同步刷入,相当于写入内存,pageCache刷入是由操作系统自己决定的。那为什么第二种方法这么慢呢?主要原因是filechannel底层读写相关的系统调用需要在内核态和用户态之间切换。请注意,这与内存复制无关。状态切换的根本原因是读写相关的系统调用本身。方法2比方法1切换了4096次,状态切换成为瓶颈,导致严重耗时。总结本阶段的要点。在DRAM中设置userwritebuffer有两个意思:方便做4kb对齐,ssdflashing友好减少用户态和内核态的切换次数,cpu友好但是mmap不同,它的底层提供了映射能力不涉及内核态和用户态之间的切换。请注意,这与内存复制无关。非切换状态的根本原因是与mmap本身相关的系统调用。这一点也很容易验证。我们用mmap的实现方法二,看看速度有多快:FileChannelfileChannel=newRandomAccessFile(file,"rw").getChannel();MappedByteBuffermap=fileChannel.map(MapMode.READ_WRITE,0,_GB);for(inti=0;i<_GB;i++){map.put((byte)0);}在我的测试机上,耗时3s,比FileChannel+4kbbufferwrite慢,但是远比FileChannelwriteSingle慢字节快。这也解释了我之前文章《文件 IO 操作的一些最佳实践》中的一个问题:“一次写入少量数据时,使用mmap会比fileChannel快很多”。其背后的原理和上面的例子是一样的。小数据量下,瓶颈不是IO,而是用户态和内核态的切换。mmap详细信息以补充写时复制模式。我们注意到publicabstractMappedByteBuffermap(MapModemode,longposition,longsize)的第一个参数,MapMode其实有三个值。上网时,几乎没有文章讲解MapMode。.MapMode有READ_WRITE、READ_ONLY、PRIVATE三个枚举值。大多数时候可能会用到READ_WRITE,而READ_ONLY只是限制了WRITE。很容易理解,但是这个PRIVATE好像有一层神秘的面纱。PRIVATE模式其实就是mmap的copyonwrite模式。当您使用MapMode.PRIVATE映射文件时,您将获得以下特性:以任何其他方式对文件进行的任何修改都将直接反映在当前的mmap映射中。privatemmap后,自身的put行为会触发replication,形成自己的copy。任何修改都不会刷到文件上,它也不会再感知到文件这一页的变化。通用名称:写时复制。这有什么用?重点是任何修改都不会闪回文件。首先,您可以获得该文件的副本。如果你正好有这个需求,可以直接使用PRIVATE模式进行映射。其次,在一个稍微激动人心的场景中,你得到一个真正的PageCache,所以不用担心它被阻塞。操作系统的闪烁会导致开销。假设你的机器配置如下:机器内存为9G,JVM参数设置为6G,heaplimit为2G,那么剩下的1G只能给内核态使用。如果你想被用户态程序使用,你可以使用mmapcopyonwritemode,它不会占用你的堆上内存或堆外内存。回收mmap内存更正了之前博文中关于mmap内存回收的错误陈述。回收mmap非常简单((DirectBuffer)mmap).cleaner().clean();mmap的生命周期可以分为:map(映射)、get/load(缺页中断)、clean(回收)。一个有用的技巧是动态分配可以在读取后异步回收的内存映射区域。mmap使用场景使用mmap处理频繁读写小数据如果IO很频繁但是数据很小,建议使用mmap避免FileChannel带来的state-cutting问题。比如索引文件的追加写入。mmapcache在使用FileChannel读写文件时,往往需要一块writecache来达到聚合的目的。内部/外部堆内存会立即丢失,这部分还没有放到磁盘上的数据也会丢失。使用mmap作为缓存会直接存储在pageCache中,不会造成数据丢失,虽然这只能避免进程被kill的情况,不能避免掉电。小文件的读写和网上很多评论恰恰相反。mmap由于其无形的特性,特别适合顺序读写,但是由于sun.nio.ch.FileChannelImpl#map(MapModemode,longposition,longsize)在size限制下,只能传一个int值。所以单个map单个文件的长度不能超过2G。如果以2G作为大小文件的门槛,那么使用mmap读写小于2G的文件一般更有优势。这在RocketMQ中也有使用。为了方便使用mmap,将commitLog的大小按照1G划分。对了,忘了说,RocketMQ等消息队列一直在使用mmap。CPU吃紧情况下的读写在大多数场景下,FileChannel和读写缓冲区的组合比mmap有优势,甚至不相上下,但是在CPU吃紧情况下读写时,使用mmap进行读写往往能发挥作用。优化效果是基于mmap不会在用户态和内核态之间切换,会导致cpu不堪重负(但这会承担动态映射和异步内存回收的开销)。持久内存Pmem、不同代次的SSD、不同主频的CPU、不同核数的CPU、不同的文件系统、文件系统挂载方式等特殊的软硬件因素。影响mmap和filechannelread/write速度的因素是因为它们对应的系统调用不同。只有基准测试通过后,我们才会知道速度。
