半岛bandao体育(中国)官方网站

重新了解 Java 中的内存映射(Mmap)

日期:2024-06-22 14:49 / 作者:zoc7RcITctunhMtq7EzA
[[434443]] Mmap的基本概念

Mmap 是一种内存映射文件的技术,即将文件映射到进程的地址空间,建立文件磁盘地址和一段进程虚拟地址的映射关系。 在建立了这种映射关系之后,进程可以直接使用指针对该内存段进行读写操作,系统会将脏页自动写回到相应的文件磁盘上,从而实现了对文件的操作,而无需再调用 read、write 等系统调用函数。反之,内核空间对该区域所做的更改会直接影响用户空间,因此可以实现不同进程之间的文件共享。mmap操作原理

操作系统提供了一系列支持mmap的函数。`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); Java中的mmap Java中原生读写方式大概可以被分为三种:普通IO,`文件通道(FileChannel)和内存映射(mmap)。区分它们也十分容易,比如FileWriter、FileReader存在于java.io包中,归属于普通IO;而FileChannel则在java.nio包中,是Java中最常用的文件操作类;而今天的重点主角mmap,则是通过FileChannel调用map方法衍生出的一种特殊文件读写方式,被称为内存映射。

使用 mmap 的方法是首先创建一个 FileChannel 对象,然后调用它的 map 方法。示例代码如下:

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 表示当前mmap指针的位置; // 通过mappedByteBuffer.put(data) 将长度为4字节的数据写入当前mmap指针的位置; // 使用mappedByteBuffer.slice() 将MappedByteBuffer切片,并将切片的位置设为指定的position; subBuffer.put(data) 将长度为4字节的数据写入切片的位置;  // 使用byte[] data = new byte[4] 来存储数据; int position = 8 表示当前mmap指针的位置; // 通过mappedByteBuffer.get(data) 从当前mmap指针的位置读取长度为4字节的数据; // 使用mappedByteBuffer.slice() 将MappedByteBuffer切片,并将切片的位置设为指定的position; subBuffer.get(data) 从切片的位置读取长度为4字节的数据; 

mmap不是解决问题的万能药{(}银弹

),这正是我写这篇文章的一个重要原因。关于 mmap 错误在网络上存在着许多误解。第一次了解 mmap 时,有很多文章都强调 mmap 适合处理大文件,现在重新审视时,发现这种观点实在荒谬,希望通过本文能够澄清 mmap 的实际应用。第一次接触 mmap 时,很多文章都强调 mmap 适用于处理大文件的情况。但现在回头看,这种观点实际上是荒谬的。希望通过本文能够澄清 mmap 的真实情况。同时存在 FileChannel 和 mmap 的情况很有可能说明两者都有适合它们的应用场景,而事实也确实如此。对于这两种工具,我们可以把它们看作是实现文件输入输出的两种方式。这两种方式本身没有好坏之分,关键在于它们的使用场景。在本节中,我们将详细比较 FileChannel 和 mmap 在文件IO中的异同之处。FileChannel 和 mmap 的读写都会经过页面缓存,更准确地说是通过监视 vmstat 排在内存中的缓存部分,而不是用户空间内存。

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 映射的这部分内存能不能称之为 pageCache,我并没有进行过调查,不过就操作系统而言,它们之间并没有太大的区别,这些 cache 都是由内核控制的。本篇文章后续也将统一使用 mmap 来表示产生的内存称为页缓存。本文将统一使用 "pageCache" 来指代通过 mmap 映射而来的内存。对于熟悉 Linux 文件 IO 的读者来说,缺页中断这个概念可能并不陌生。mmap和FileChannel都使用缺页中断的方式来进行文件读写。以 mmap 读取 1G 文件为例,使用fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB);进行映射是一项开销非常小的操作,并不表示1G的文件已经被加载到pageCache中。只有采取以下措施,才能保证将文件读入页面缓存。

文 档渠 道 fileChannel = 新 RandomAccessFile(file, "rw").getChannel(); 映射字节缓冲区 map = fileChannel.map(MapMode.READ_WRITE, 0, _GB); for (int i = 0; i < _GB; i += _4kb) {  temp += map.get(i); } 
有关内存对齐的详细信息不在此展开,可参考 java.nio.MappedByteBuffer#load 方法。load 方法也是通过按页访问的方式触发中断

以下是 pageCache 逐渐增长的过程,共约增长了 1.034G,表明文件内容已全部加载。

处理器 --内存-- ---交换空间--- -----I/O----- -系统-  ------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      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 

两个方面需要注意:

mmap映射的过程可以视为一种延迟加载。 只有在调用get()方法时才会引发缺页中断

预读的大小是由操作系统算法决定的,默认为4kb。如果希望将延迟加载变成实时加载,需要按照步长为4kb进行一次遍历
同时,FileChannel的缺页中断原理与此相同,都需要利用PageCache作为中转,完成文件的读写。

内存复制的次数

是一个经常被讨论的话题。有很多人认为相对于FileChannel,mmap少了一次复制,但我个人认为这取决于具体的场景。

内存复制次数

重新了解 Java 中的内存映射(Mmap)

在很多讨论中认为 mmap 与 FileChannel 相比少了一次复制,我个人认为仍需要根据具体情况进行区分。
例如,如果需求是从文件的起始位置读取一个整数,那么这两种方式其实经历的路径是相同的:SSD -> pageCache -> 应用程序内存,因此 mmap 并不会减少一次复制。假设需要维护一个100兆的复用缓冲区并涉及文件I/O,那么可以直接使用mmap将其当作100兆的缓冲区来使用,而不需要在进程的内存(用户空间)中额外维护一个100兆的缓冲区。用户态和内核态是操作系统用来分离可执行程序和操作系统的一种机制。这种机制确保操作系统能控制对硬件的访问,从而提高系统的安全性。操作系统为用户提供了系统调用接口,用于调用底层的功能,并在用户态和内核态之间进行切换。这涉及到“用户态”和“内核态”的切换问题,我认为这个概念对很多人来说容易产生混淆,我在这里想整理一下我个人的理解,如果有错误欢迎指正BOB半岛入口。请先查看FileChannel。你认为下面的两段代码,哪一段更快呢?

// 第一种方法: 使用 4kb 缓冲 刷写 FileChannel fileChannel = new RandomAccessFile(file,```plaintext\nFileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();\nByteBuffer byteBuffer = ByteBuffer.allocateDirect(_4kb);\nfor (int i = 0; i < _4kb; i++) {\n byteBuffer.put((byte)0);\ \nfor (int i = 0; i < _GB; i += _4kb) {\n byteBuffer.position(0);\n byteBuffer.limit(_4kb);\n fileChannel.write(byteBuffer);\ \n// 方法二: 单字节刷盘\n```使用代码 ``rw`` 获取通道。通过 ByteBuffer 类创建一个 byteBuffer 对象,并分配 1 个字节的直接内存空间。向 byteBuffer 对象中放入值为 0 的字节。然后执行循环,将 byteBuffer 中的内容写入文件通道。循环的次数为 _GB。方法一使用:刷新 4KB 缓冲区到磁盘(常规操作)。我在测试机器上只需要1.2秒就写入了1G的数据。而且没有使用任何缓存的方法,几乎完全卡住了,文件增长速度非常慢,在等待了 5 分钟后还没有写入完毕,所以中断了测试。不使用任何缓冲方法来进行第二次尝试几乎让程序直接卡死,文件增长速度极其缓慢。在等待了5分钟仍未完成写入后,测试被中断。使用写入缓冲区是一个经典的优化技巧,用户只需要设置4kb的整数倍的写入缓冲区,就能够将小数据写入进行聚合,从而使得数据在刷盘时尽可能以4kb的整数倍进行,避免写入放大问题。然而,这并不是本节课的重点。大家有没有考虑过,pageCache 本质上也是一种缓存,实际上写入 1 字节并不会立即同步到磁盘,而是先写入内存,pageCache 的刷盘操作由操作系统自行决定。为什么方法二的速度这么慢呢?主要是因为filechannel的read/write底层相关的系统调用需要在内核态和用户态之间切换,需要注意的是,这与内存拷贝没有任何关系,导致态切换的根本原因是read/write关联的系统调用本身。方案二相比方案一多了4096倍的切换次数,因此状态的切换成为了瓶颈,导致执行时间严重增加。在 DRAM 中设置用户写入缓冲区有两个重要意义需要总结一下:首先,它方便进行 4kb 对齐,有利于 SSD 刷盘。其次,它可以减少用户态和内核态的切换次数,有利于 CPU 的性能。然而,与 mmap 不同的是,mmap 的底层映射能力不会导致内核态和用户态的切换。需要注意的是,这与内存拷贝没有直接关系,根本原因在于 mmap 关联的系统调用本身。验证这一点,也很简单,我们可以通过使用 mmap 实现方法二来检测速度:

FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0,在我的测试机器上,执行以下代码花费了3秒: \n```java\nfor(int i=0; i<_GB; i++){\n map.put((byte)0);\ \n```\n相较于使用FileChannel和4KB缓冲进行写操作,这种方式慢了一些;但是相较于使用FileChannel进行单字节写操作,它要快得多。在我之前的文章《文件IO操作的一些建议》中也解释了这个问题:“对于小数据量写入场景,使用mmap会比fileChannel快得多”,背后的原理和上面的例子一样,在处理小数据量时,性能瓶颈不在于IO,而在于用户态和内核态的切换。在讨论`n`细节时, 我们发现copy on write模式。要注意`x` public abstract MappedByteBuffer map(MapMode mode,long position, long size)`n`的第一个参数。`x`MapMode`x`实际上有三个值。在网络上几乎找不到对`nMapMode`的解释文章。MapMode这个枚举拥有三种取值:READ_WRITE、READ_ONLY和PRIVATE。通常情况下,可能最常用的是READ_WRITE,而READ_ONLY只是限制了写操作,这很容易理解。但PRIVATE似乎带有一层神秘的意味。MapMode有三个枚举值:READ_WRITE、READ_ONLY、PRIVATE。通常情况下,最常使用的是READ_WRITE,READ_ONLY则只是限制了写入操作,这很容易理解。但PRIVATE模式似乎有一层神秘面纱。事实上,PRIVATE模式就是mmap的写时复制模式。当使用MapMode.PRIVATE去映射文件时,你会获得以下特性:其他任何对文件的修改都会直接反映在当前mmap映射中。在使用私有映射后,对其进行写入操作会触发复制,生成自身的副本。所有的修改不会被写入文件中,也不会再感知到文件页面的更改。
通常被称为“写时复制”。

有什么作用呢?关键是任何修改都不会导致文件回退。首先,如果您需要获得文件副本,可以直接使用私有模式进行映射获取,其次,当您获得真正的PageCache时,会让人有些兴奋,不必担心操作系统刷新导致额外开销。假设您的计算机配置如下:内存为9G,JVM设置为6G,堆外限制为2G,剩下的1G只能被内核态使用。如果要让用户态程序使用这部分内存,可以采用mmap的写时复制模式,这样就不会占用堆内存或堆外内存。为了更正之前关于 mmap 内存回收的错误说法,对于 mmap 内存的回收非常简单。简单来说,mmap 的生命周期可以分为三个阶段:映射,获取/加载(缺页中断),回收。要回收 mmap 内存,只需执行((DirectBuffer)mmap).cleaner().clean();。一种实用的方法是对内存进行动态分配,并在读取后进行异步回收。mmap被广泛应用于处理频繁读写小数据的场景。当IO操作非常频繁,但数据量却很小时,推荐使用mmap,以规避FileChannel可能导致的切换状态问题BOB半岛新版。比如说在索引文件中进行追加内容的写入操作。在使用 FileChannel 进行文件读写时,通常需要一个写入缓存来集中数据。最常见的是使用堆内存或堆外内存,但它们都有一个共同的问题,那就是当进程意外终止时,堆内存或堆外内存中的数据会立即丢失,导致部分数据没有被写入磁盘而丢失。使用 mmap 作为缓存时,数据会直接存储在pageCache中,这样就不会导致数据丢失。尽管这只能避免进程被 kill 这种情况,却无法避免电力故障。「n小文件的读写n」与网上传播的很多言论刚好相反。由于其非常适合顺序读写,mmap因为其不切实际的特点,但是由于sun.nio.ch.FileChannelImpl#map(MapMode mode, long position, long size) 中size的限制,只能传递一个int值。所以,单次map单个文件的长度不能超过2G。如果将2G作为文件大小的阈值,那么小于2G的文件使用mmap来读写一般来说是有优势的。RocketMQ 也运用了这一特点,以便更便于使用 mmap,将 commitLog 划分为 1G 的大小。是的,我忘了提到,RocketMQ 等消息队列一直在使用 mmap 技术。在大多数情况下,当CPU紧张时,FileChannel和读写缓冲的组合通常比mmap更具优势,或者说并无显著差异。但在CPU紧张的读写情况下,采用mmap进行读写通常能够带来优化效果。这是因为mmap不需要用户态和内核态的切换,从而减轻CPU的负担(虽然会增加动态映射和异步回收内存的开销)。软硬件因素的特殊性比如持久性内存 Pmem,不同代数的固态硬盘 SSD,不同主频的中央处理器 CPU,不同核数的 CPU,不同的文件系统,文件系统的挂载方式等等因素都可能会影响 mmap 和文件通道读写的速度,因为它们对应的系统调用是不同的。只有经过基准测试后,才能知道其速度快慢。

BOB半岛娱乐

BOB半岛官方

BOB半岛官方

BOB半岛娱乐


BOB半岛下载 BOB半岛入口 BOB半岛APP