重新认识 Java 中的内存映射(mmap)

news2024/9/25 7:20:37

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 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 指针的位置写入 4b 的数据
mappedByteBuffer.put(data);
// 指定 position 写入 4b 的数据
MappedByteBuffer subBuffer = mappedByteBuffer.slice();
subBuffer.position(position);
subBuffer.put(data);

// 读
byte[] data = new byte[4];
int position = 8;
// 从当前 mmap 指针的位置读取 4b 的数据
mappedByteBuffer.get(data);
// 指定 position 读取 4b 的数据
MappedByteBuffer subBuffer = mappedByteBuffer.slice();
subBuffer.position(position);
subBuffer.get(data);

mmap 不是银弹
FileChannel 与 mmap 同时存在,大概率说明两者都有其合适的使用场景,而事实也的确如此。在看待二者时,可以将其看待成实现文件 IO 的两种工具,工具本身没有好坏,主要还是看使用场景。

mmap vs FileChannel
这一节,详细介绍一下 FileChannel 和 mmap 在进行文件 IO 的一些异同点。

pageCache
FileChannel 和 mmap 的读写都经过 pageCache,或者更准确的说法是通过 vmstat 观测到的 cache 这一部分内存,而非用户空间的内存。

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。

缺页中断
对 Linux 文件 IO 有基础认识的读者,可能对缺页中断这个概念也不会太陌生。mmap 和 FileChannel 都以缺页中断的方式,进行文件读写。

以 mmap 读取 1G 文件为例, fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); 进行映射是一个消耗极少的操作,此时并不意味着 1G 的文件被读进了 pageCache。只有通过以下方式,才能够确保文件被读进 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);
}

关于内存对齐的细节在这里就不拓展了,可以详见 java.nio.MappedByteBuffer#load 方法,load 方法也是通过按页访问的方式触发中断

如下是 pageCache 逐渐增长的过程,共计约增长了 1.034G,说明文件内容此刻已全部 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

两个细节:
mmap 映射的过程可以理解为一个懒加载, 只有 get() 时才会触发缺页中断
预读大小是有操作系统算法决定的,可以默认当作 4kb,即如果希望懒加载变成实时加载,需要按照 step=4kb 进行一次遍历
而 FileChannel 缺页中断的原理也与之相同,都需要借助 PageCache 做一层跳板,完成文件的读写。

内存拷贝次数
很多言论认为 mmap 相比 FileChannel 少一次复制,我个人觉得还是需要区分场景。

例如需求是从文件首地址读取一个 int,两者所经过的链路其实是一致的:SSD -> pageCache -> 应用内存,mmap 并不会少拷贝一次。

但如果需求是维护一个 100M 的复用 buffer,且涉及到文件 IO,mmap 直接就可以当做是 100M 的 buffer 来用,而不用在进程的内存(用户空间)中再维护一个 100M 的缓冲。

用户态与内核态
在这里插入图片描述

用户态和内核态
操作系统出于安全考虑,将一些底层的能力进行了封装,提供了系统调用(system call)给用户使用。这里就涉及到“用户态”和“内核态”的切换问题,私认为这里也是很多人概念理解模糊的重灾区,我在此梳理下个人的认知,如有错误也欢迎指正。

先看 FileChannel,下面两段代码,你认为谁更快?

// 方法一: 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);
}

// 方法二: 单字节刷盘
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 缓冲刷盘(常规操作),在我的测试机器上只需要 1.2s 就写完了 1G。而不使用任何缓冲的方法二,几乎是直接卡死,文件增长速度非常缓慢,在等待了 5 分钟还没写完后,中断了测试。

使用写入缓冲区是一个非常经典的优化技巧,用户只需要设置 4kb 整数倍的写入缓冲区,聚合小数据的写入,就可以使得数据从 pageCache 刷盘时,尽可能是 4kb 的整数倍,避免写入放大问题。

但这不是这一节的重点,大家有没有想过,pageCache 其实本身也是一层缓冲,实际写入 1byte 并不是同步刷盘的,相当于写入了内存,pageCache 刷盘由操作系统自己决策。那为什么方法二这么慢呢?主要就在于 filechannel 的 read/write 底层相关联的系统调用,是需要切换内核态和用户态的,注意,这里跟内存拷贝没有任何关系,导致态切换的根本原因是 read/write 关联的系统调用本身。方法二比方法一多切换了 4096 倍,态的切换成为了瓶颈,导致耗时严重。

阶段总结一下重点,在 DRAM 中设置用户写入缓冲区这一行为有两个意义:

方便做 4kb 对齐,ssd 刷盘友好
减少用户态和内核态的切换次数,cpu 友好
但 mmap 不同,其底层提供的映射能力不涉及到切换内核态和用户态,注意,这里跟内存拷贝还是没有任何关系,导致态不发生切换的根本原因是 mmap 关联的系统调用本身。验证这一点,也非常容易,我们使用 mmap 实现方法二来看看速度如何:

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);
}

在我的测试机器上,花费了 3s,它比 FileChannel + 4kb 缓冲写要慢,但远比 FileChannel 写单字节快。

这里也解释了我之前文章《文件 IO 操作的一些最佳实践》中一个疑问:"一次写入很小量数据的场景使用 mmap 会比 fileChannel 快的多“,其背后的原理就和上述例子一样,在小数据量下,瓶颈不在于 IO,而在于用户态和内核态的切换。

mmap 细节补充
copy on write 模式
我们注意到 public abstract MappedByteBuffer map(MapMode mode,long position, long size) 的第一个参数,MapMode 其实有三个值,在网络冲浪的时候,也几乎没有找到讲解 MapMode 的文章。MapMode 有三个枚举值 READ_WRITE、READ_ONLY、PRIVATE,大多数时候使用的可能是 READ_WRITE,而 READ_ONLY 不过是限制了 WRITE 而已,很容易理解,但这个 PRIVATE 身上似乎有一层神秘的面纱。

实际上 PRIVATE 模式正是 mmap 的 copy on write 模式,当使用 MapMode.PRIVATE 去映射文件时,你会获得以下的特性:

其他任何方式对文件的修改,会直接反映在当前 mmap 映射中。
private mmap 之后自身的 put 行为,会触发复制,形成自己的副本,任何修改不会会刷到文件中,也不再感知该文件该页的改动。
俗称:copy on write。

这有什么用呢?重点就在于任何修改都不会回刷文件。其一,你可以获得一个文件副本,如果你正好有这个需求,直接可以使用 PRIVATE 模式去进行映射,其二,令人有点小激动的场景,你获得了一块真正的 PageCache,不用担心它会被操作系统刷盘造成 overhead。假设你的机器配置如下:机器内存 9G,JVM 参数设置为 6G,堆外限制为 2G,那剩下的 1G 只能被内核态使用,如果想被用户态的程序利用起来,就可以使用 mmap 的 copy on write 模式,这不会占用你的堆内内存或者堆外内存。

回收 mmap 内存
更正之前博文关于 mmap 内存回收的一个错误说法,回收 mmap 很简单

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

mmap 的生命中简单可以分为:map(映射),get/load (缺页中断),clean(回收)。一个实用的技巧是动态分配的内存映射区域,在读取过后,可以异步回收掉。

mmap 使用场景
1 使用 mmap 处理小数据的频繁读写
如果 IO 非常频繁,数据却非常小,推荐使用 mmap,以避免 FileChannel 导致的切态问题。例如索引文件的追加写。

2 mmap 缓存
当使用 FileChannel 进行文件读写时,往往需要一块写入缓存以达到聚合的目的,最常使用的是堆内/堆外内存,但他们都有一个问题,即当进程挂掉后,堆内/堆外内存会立刻丢失,这一部分没有落盘的数据也就丢了。而使用 mmap 作为缓存,会直接存储在 pageCache 中,不会导致数据丢失,尽管这只能规避进程被 kill 这种情况,无法规避掉电。

3 小文件的读写
恰恰和网传的很多言论相反,mmap 由于其不切态的特性,特别适合顺序读写,但由于 sun.nio.ch.FileChannelImpl#map(MapMode mode, long position, long size) 中 size 的限制,只能传递一个 int 值,所以,单次 map 单个文件的长度不能超过 2G,如果将 2G 作为文件大 or 小的阈值,那么小于 2G 的文件使用 mmap 来读写一般来说是有优势的。在 RocketMQ 中也利用了这一点,为了能够方便的使用 mmap,将 commitLog 的大小按照 1G 来进行切分。对的,忘记说了,RocketMQ 等消息队列一直在使用 mmap。

4 cpu 紧俏下的读写
在大多数场景下,FileChannel 和读写缓冲的组合相比 mmap 要占据优势,或者说不分伯仲,但在 cpu 紧俏下的读写,使用 mmap 进行读写往往能起到优化的效果,它的根据是 mmap 不会出现用户态和内核态的切换,导致 cpu 的不堪重负(但这样承担起动态映射与异步回收内存的开销)。

5 特殊软硬件因素
例如持久化内存 Pmem、不同代数的 SSD、不同主频的 CPU、不同核数的 CPU、不同的文件系统、文件系统的挂载方式…等等因素都会影响 mmap 和 filechannel read/write 的快慢,因为他们对应的系统调用是不同的。只有 benchmark 过后,方知快慢。

转载自:https://cloud.tencent.com/developer/article/1902272

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/382501.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电源程控软件下载安装教程

软件&#xff1a;电源程控软件NS-PowerSupply 语言&#xff1a;简体中文 环境&#xff1a;NI-VISA 安装环境&#xff1a;Win10以上版本&#xff08;特殊需求请后台私信联系客服&#xff09; 硬件要求&#xff1a;CPU2GHz 内存4G(或更高&#xff09;硬盘500G(或更高&#xf…

2023年2月安全事件盘点

一、基本信息 2023年2月安全事件共造成约3796万美元损失&#xff0c;相较于上个月&#xff0c;安全事件数量与损失金额都有显著上升&#xff0c;其中Platypus Finance闪电贷攻击为单次利用损失之最高达850万美元。本月RugPull数量基本与上月持平&#xff0c;损失金额占比显著降…

网站打不开数据库错误等常见问题解决方法

1、“主机开设成功&#xff01;”上传数据后显示此内容&#xff0c;是因为西部数码默认放置的index.htm内容&#xff0c;需要核实wwwroot目录里面是否有自己的程序文件&#xff0c;可以删除index.htm。 2、恭喜&#xff0c;lanmp安装成功&#xff01;这个页面是wdcp的默认页面&…

用 Real-ESRGAN 拯救座机画质,自制高清版动漫资源

内容一览&#xff1a;Real-ESRGAN 是 ESRGAN 升级之作&#xff0c;主要有三点创新&#xff1a;提出高阶退化过程模拟实际图像退化&#xff0c;使用光谱归一化 U-Net 鉴别器增加鉴别器的能力&#xff0c;以及使用纯合成数据进行训练。 关键词&#xff1a;Real-ESRGAN 超分辨率 视…

一文彻底搞懂cookie、session、token、jwt!

前言 随着Web应用程序的出现&#xff0c;直接在客户端上存储用户信息的需求也随之出现。者背后的想象时合法的&#xff1a;与特定用户相关的信息都应该保存在用户的机器上。无论是登录信息、个人偏好、还是其他数据&#xff0c;Web应用程序提供者都需要有办法 将他们保存在客户…

电子技术——CMOS 逻辑门电路

电子技术——CMOS 逻辑门电路 在本节我们介绍如何使用CMOS电路实现组合逻辑函数。在组合电路中&#xff0c;电路是瞬时发生的&#xff0c;也就是电路的输出之和当前的输入有关&#xff0c;并且电路是无记忆的也没有反馈。组合电路被大量的使用在当今的数字逻辑系统中。 晶体管…

Educational Codeforces Round 144 (Rated for Div. 2)(A~C)

A. Typical Interview Problem从1开始&#xff0c;遇到3的倍数就在字符串后面加F&#xff0c;遇到5的倍数就在字符串后面加B&#xff0c;若遇到3和5的倍数&#xff0c;就加入FB&#xff0c;这样可以写一个无限长的字符串&#xff0c;给出一个长度最多为10的字符串&#xff0c;判…

CLion+Opencv+QT开发相关

一、QT安装和配置其实我并没有直接在Qt上开发&#xff0c;下载Qt而是因为&#xff1a;CLion可以通过Qt的MinGW作为Toolset&#xff0c;并且可以将Qt creator作为external tool&#xff1b;在进行Opencv的编译安装中可以用Qt自带的MinGW进行编译和安装&#xff0c;不用另外下载M…

C++类和对象:初始化列表、static成员和友元

目录 一. 初始化列表 1.1 对象实例化时成员变量的创建及初始化 1.2 初始化列表 1.3 使用初始化列表和在函数体内初始化成员变量的效率比较 1.4 成员变量的初始化顺序 1.5 explicit关键字 二. static成员 2.1 static属性的成员变量 2.2 static属性的成员函数 三. 友元 …

废气处理设备远程监控

当今工业迅速的发展&#xff0c;工业带给人们的经济效益显著&#xff0c;而同时污染问题也备受关注。国家环保标准对排放至大气的废气指标提出了更高的要求。面临着环保压力&#xff0c;企业为走可持续发展之路&#xff0c;为维护员工利益、改善工作环境及周边环境不受影响&…

一、Sping框架引入

OCP开闭原则 什么是OCP&#xff1f; OCP是软件七大开发原则当中最基本的一个原则&#xff1a;开闭原则 对什么开&#xff1f;对扩展开放。 对什么闭&#xff1f;对修改关闭。OCP原则是最核心的&#xff0c;最基本的&#xff0c;其他的六个原则都是为这个原则服务的。OCP开闭原则…

计算机行业回暖?看网友怎么说?

就业寒潮之下&#xff0c;去年的应届生们可谓哀嚎一片&#xff0c;不少人晒出自己的0offer秋招战绩。 就连过去无往不利的计算机行业&#xff0c;亦不例外。但今年开始&#xff0c;计算机行业逐渐有了回暖的迹象和讨论。 陆续有不少之前哭诉收获惨淡的计算机专业同学&#x…

防静电和浪涌TVS layout设计要点

电子产品精密化刚看过了CES2023&#xff0c;雷卯的外贸伙伴们看了最新的AR,VR,5G产品&#xff0c;新的电子产品更智能、更复杂&#xff0c;嵌入了脆弱和敏感的集成电路。这些设备的环境往往很恶劣&#xff0c;产生高水平静电和快速瞬态浪涌。这些ESD事件可能会干扰设备&#xf…

IIS之web服务器的安装、部署以及使用教程(图文详细版)

WEB服务器的部署 打开虚拟机后查看已经开放的端口&#xff0c;可以看到没有TCP 80、TCP 443&#xff0c;说明HTTP服务端口没有打开 打开我的电脑—双击CD驱动器 选择安装可选的Windows组件 选择应用程序服务器—打开Internet信息服务—选择万维网服务和FTP服务 一路确…

uniapp-首页配置

为了获取到后台服务器发来的数据&#xff0c;需要配置相应的网络地址。位置在main.js入口文件中。 import { $http } from escook/request-miniprogramuni.$http $http // 配置请求根路径 $http.baseUrl https://api-hmugo-web.itheima.net// 请求开始之前做一些事情 $http.…

Spring-Xml配置

一、Spring 简介 1.简介 文档下载地址&#xff1a;Index of /spring-framework/docs 1.简介 Spring framework 是 Spring 基础框架 学习Spring 家族产品 Spring framework SpringBoot SpringCloud Spring 能用来做什么 开发 WEB 项目 微服务 分布式系统 Spring framew…

云服务HCIE变题当天一把过!分享下学习备考和考试经验

大家好&#xff0c;我是誉天云服务学员刘同学。感谢在誉天的学习&#xff0c;让我在临考变题的情况下通过了云服务HCIE考试&#xff1b;也感谢誉天给我这次机会分享出学习备考和考试的经验。 算起来&#xff0c;我和誉天也是老朋友了&#xff1a;一开始是跟着邹老师学习云计算、…

【JAVA】线程和进程

&#x1f3c6;今日学习目标&#xff1a;线程和进程 &#x1f603;创作者&#xff1a;颜颜yan_ ✨个人主页&#xff1a;颜颜yan_的个人主页 ⏰本期期数&#xff1a;第三期 &#x1f389;专栏系列&#xff1a;JAVA 线程和进程前言一、进程与线程1.进程2.线程二、线程的创建2.1 继…

Pytest自动化框架~权威教程03-原有TestSuite的执行方法

前言TestSuite一直是unittest的灵活与精髓之处, 在繁多的测试用例中, 可以任意挑选和组合各种用例集, 比如smoke用例集, level1用例集, webtest用例集, bug回归用例集等等, 当然这些TestSuite需要我们提前定义好, 并把用例加载进去.Pytest采取的是完全不同的用例组织和运行方式…

有些笑话,外行人根本看不懂,只有程序员看了会狂笑不止

我一直都觉得我们写代码的程序员与众不同&#xff0c;就连笑话都跟别人不一样。 如果让外行人来看我们一些我们觉得好笑的东西&#xff0c;他们根本不知道笑点在哪里。 不信你来瞧瞧&#xff0c;但凡有看不懂的地方&#xff0c;说明你的道行还不够深。 1.大多数人开始学编程时…