当前位置: 首页 > news >正文

【源码解析】Java NIO 包中的 MappedByteBuffer

文章目录

  • 1. 前言
  • 2. MappedByteBuffer
  • 3. 例子
  • 4. 属性
  • 5. 构造器
  • 6. mappingOffset、mappingAddress、mappingLength
  • 7. isLoaded 判断内存是否还在内存中
  • 8. load 方法将 ByteBuffer 加载到 Page Cache 中
  • 9. force 刷盘


1. 前言

上一篇文章我们介绍了 HeapByteBuffer 的源码,这篇文章我们来介绍下 MappedByteBuffer,这个 MappedByteBuffer 是 DirectByteBuffer 的父类。

  • 【源码解析】Java NIO 包中的 Buffer
  • 【源码解析】Java NIO 包中的 ByteBuffer
  • 【源码解析】Java NIO 包中的 HeapByteBuffer


2. MappedByteBuffer

MappedByteBuffer 是 ByteBuffer 的子类,表示一个直接字节缓冲区,其内容是文件的内存映射区域。通过 MappedByteBuffer,程序可以直接对文件内容进行读写操作,而无需通过传统的 I/O 流或通道。

相比传统的文件 IO 操作,比如 FileInputStreamFileOutputStream,这种方式可以直接对内存中的数据进行操作,操作系统会负责将内存中的更改同步到磁盘文件中。

MappedByteBuffer 通过 FileChannel 的 map 方法创建,创建的时候可以设置三种模式:

  1. MapMode.READ_ONLY:只读模式,映射的缓冲区是只读的
  2. MapMode.READ_WRITE:读写模式,映射的缓冲区是可读写的,对缓冲区的修改会同步到文件中
  3. MapMode.PRIVATE:私有模式,映射的缓冲区是可写的,但修改不会同步到文件中,而是创建一个私有副本

但是由于 MappedByteBuffer 使用的是堆外内存,所以如果尝试映射过大的文件,可能会导致内存不足(OutOfMemoryError),毕竟内存映射文件的大小受操作系统和可用物理内存的限制。

所以最后总结一下,当需要频繁读写大文件,或者需要随机文件访问的时候就可以使用这个 MappedByteBuffer。


3. 例子

首先我们需要生成一个 1G 的文件。

public class FileTest {public static void main(String[] args) {String filePath = "D:\\学习资料\\计算机编程语言java学习\\后台\\JDK源码\\jdk1.8Source\\src\\test\\file\\hello.txt"; // 生成的文件路径long fileSizeInBytes = 1024L * 1024 * 1024; // 1GBtry {generateFile(filePath, fileSizeInBytes);System.out.println("文件生成成功!路径: " + filePath);} catch (IOException e) {System.err.println("文件生成失败: " + e.getMessage());}}/*** 生成指定大小的文件,内容为 "helloWorld" 的重复填充** @param filePath      文件路径* @param fileSizeInBytes 文件大小(字节)* @throws IOException 如果写入失败*/public static void generateFile(String filePath, long fileSizeInBytes) throws IOException {// "helloWorld" 的字节数byte[] content = "helloWorld".getBytes();int contentLength = content.length;try (FileOutputStream fos = new FileOutputStream(filePath);BufferedOutputStream bos = new BufferedOutputStream(fos)) {// 写入次数long len = fileSizeInBytes / contentLength;// 一次写入 helloWorld 字节数for (long i = 0; i < len; i++) {bos.write(content);}// 剩余字节long remainingBytes = fileSizeInBytes % contentLength;if (remainingBytes > 0) {bos.write(content, 0, (int) remainingBytes);}}}
}

生成了 hello.txt 之后,可以看下面图。
在这里插入图片描述
生成 1G 的文件之后我们再来看下传统的 IO 读取数据和 MappedByteBuffer 读取数据的效率。

public class MappedByteBufferPerformance {public static void main(String[] args) throws Exception {String filePath = "D:\\学习资料\\计算机编程语言java学习\\后台\\JDK源码\\jdk1.8Source\\src\\test\\file\\hello.txt"; // 生成的文件路径long fileSize = 1024 * 1024 * 1024; // 1GBlong startTime = System.currentTimeMillis();try (RandomAccessFile file = new RandomAccessFile(filePath, "r");FileChannel channel = file.getChannel()) {MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);// 读取文件内容while (buffer.hasRemaining()) {buffer.get(); // 读取一个字节}}long endTime = System.currentTimeMillis();System.out.println("MappedByteBuffer 读取时间: " + (endTime - startTime) + " ms");}
}

首先上面是 MappedByteBuffer 的读取,总共用了 317 ms,如下图所示。
在这里插入图片描述

下面我们再来看下使用传统 IO 来读取文件的耗时。

public class BufferedIOPerformance {public static void main(String[] args) throws Exception {String filePath = "D:\\学习资料\\计算机编程语言java学习\\后台\\JDK源码\\jdk1.8Source\\src\\test\\file\\hello.txt"; // 生成的文件路径long startTime = System.currentTimeMillis();try (FileInputStream fis = new FileInputStream(filePath);BufferedInputStream bis = new BufferedInputStream(fis)) {// 读取文件内容while (bis.read() != -1) {// 读取一个字节}}long endTime = System.currentTimeMillis();System.out.println("普通 I/O 读取时间: " + (endTime - startTime) + " ms");}
}

普通 IO 的读取耗时如下:
在这里插入图片描述
所以这里总结下读取的结果:

操作方式文件大小读取时间备注
MappedByteBuffer1 GB317 ms直接内存映射,效率极高
BufferedInputStream1 GB19552 ms带缓冲区的普通 I/O,速度较慢

4. 属性

MappedByteBuffer 中只有一个属性 fd,其他属性都在父类 ByteBuffer 中。

private final FileDescriptor fd; 是 Java 中用于表示操作系统文件描述符的对象。它允许 Java 程序与底层的文件系统进行交互。说白了这玩意就是用来映射文件到内存的。


5. 构造器

MappedByteBuffer(int mark, int pos, int lim, int cap, // package-privateFileDescriptor fd)
{super(mark, pos, lim, cap);this.fd = fd;
}MappedByteBuffer(int mark, int pos, int lim, int cap) {super(mark, pos, lim, cap);this.fd = null;
}

这两个构造器其实就是一个指定了文件描述符,一个没有指定。


6. mappingOffset、mappingAddress、mappingLength

private long mappingOffset() {// 页大小int ps = Bits.pageSize();// 求直接内存的偏移量long offset = address % ps;// 确保一定是正数return (offset >= 0) ? offset : (ps + offset);
}private long mappingAddress(long mappingOffset) {// address 表示缓冲区的起始地址// mappingOffset 是上面的偏移量return address - mappingOffset;
}private long mappingLength(long mappingOffset) {return (long)capacity() + mappingOffset;
}

第一个方法 mappingOffset 获取的是 MappedByteBuffer 的内存地址相对于内存页面起始位置的偏移量, Bits.pageSize(): 这里面返回的是操作系统的内存分页大小,一般是 4KB 或者 8KB,这里取决于用什么操作系统。在进行内存映射的时候可以用这个方法求出偏移量来进行内存对齐。

第二个方法 mappingAddress 用来计算内存页面的起始地址,这里的 mappingOffset 一般就是上面的 mappingOffset 方法。address - mappingOffset 这个方法就是使用缓冲区 ByteBuffer 的起始地址减去偏移量。

第三个方法 mappingLength 求出的是内存映射文件的总长度,也就是 mmap 文件映射的内容区域。

上面这几个方法就是获取 MappedByteBuffer 的各种地址信息,那为什么又要有一个偏移量呢?我们知道操作系统分配最小单位是一个页,所以当使用 mmap 映射操作系统内存的时候分配的内存总是一个页的起始位置。
在这里插入图片描述
虽然我们获取了 MappedByteBuffer,但是这个 MappedByteBuffer 的起始位置有可能不是一个页的起始位置,也就是说上面图中 mappingAddress 是页的起始位置,但是 MappedByteBuffer 里面的起始地址是 address。操作系统分配内存肯定是一个页来分配的,所以 MappedByteBuffer 的起始地址和实际分配的有可能不一样,相差就是 mappingOffset。上面的 mappingOffset 求出来的就是 mappingAddress -> address 之间的距离,而 mappingAddress 求出来的就是操作系统内核实际调用 mmap 分配的内存页起点,就是上图的 mappingAddress,最后一个方法 mappingLength 求出来的就是 mmap 实际分配的内存容量。
在这里插入图片描述

7. isLoaded 判断内存是否还在内存中

public final boolean isLoaded() {// 判断 MappedByteBuffer 有没有映射到一个文件checkMapped();// 如果起始地址为 0 或者容量为 0if ((address == 0) || (capacity() == 0))// 表示已经不在物理内存里面了return true;// 获取 mmap 分配的内存的起始位置,也就是图中的 mappingOffsetlong offset = mappingOffset();// MappedBuffer 实际映射的内存区域大小 也是 mmap 实际分配的内存大小long length = mappingLength(offset);// mappingAddress(offset) 获取实际的映射起始位置 mapPosition// Bits.pageCount(length) 表示分配了多少个页// 调用 native 方法return isLoaded0(mappingAddress(offset), length, Bits.pageCount(length));
}private native boolean isLoaded0(long address, long length, int pageCount);
  • 如果结果是 true,表示缓冲区的内容很可能已经驻留在物理内存中,访问这些数据时不会触发虚拟内存页错误或 I/O 操作。
  • 如果返回 false,并不一定表示缓冲区的内容没有驻留在物理内存中,可能只是部分数据不在物理内存中。

8. load 方法将 ByteBuffer 加载到 Page Cache 中

public final MappedByteBuffer load() {// 判断文件描述符是不是空checkMapped();if ((address == 0) || (capacity() == 0))return this;// 获取 mmap 内存地址到 MappedByteBuffer 的距离long offset = mappingOffset();// 获取 mmap 分配的内存长度long length = mappingLength(offset);// 调用 native 将 MappedByteBuffer 中的内容预读到 page cache 中load0(mappingAddress(offset), length);// Read a byte from each page to bring it into memory. A checksum// is computed as we go along to prevent the compiler from otherwise// considering the loop as dead code.Unsafe unsafe = Unsafe.getUnsafe();// 一个页的大小int ps = Bits.pageSize();// 这个 ByteBuffer 分配了多少个页int count = Bits.pageCount(length);// 获取 mmap 映射地址的起始地址long a = mappingAddress(offset);byte x = 0;// 从 mmap 起始地址开始遍历所有页,每遍历一次访问一下都会发生缺页中断,// 同时将 MappedByteBuffer 和 Page Cache 进行页表映射for (int i=0; i<count; i++) {x ^= unsafe.getByte(a);a += ps;}if (unused != 0)unused = x;return this;
}

这个方法会将 ByteBuffer 内容里面的数据加载到 Page Cache 中,并且这个方法还会遍历所有页预读一次。因为数据加载到 Page Cache 之后,并不会立刻就生成虚拟内存到物理内存的映射。所以加载到 Page Cache 的物理页之后需要访问一次发生缺页中断,这时候才会生成页表项。


9. force 刷盘

public final MappedByteBuffer force() {checkMapped();if ((address != 0) && (capacity() != 0)) {// 核心逻辑,从 mmap 映射的起始位置开始,将映射的内容进行刷盘long offset = mappingOffset();force0(fd, mappingAddress(offset), mappingLength(offset));}return this;
}private native void force0(FileDescriptor fd, long address, long length);

这个方法就是将 Buffer 里面的数据进行刷盘。





如有错误,欢迎指出!!!

相关文章:

【源码解析】Java NIO 包中的 MappedByteBuffer

文章目录 1. 前言2. MappedByteBuffer3. 例子4. 属性5. 构造器6. mappingOffset、mappingAddress、mappingLength7. isLoaded 判断内存是否还在内存中8. load 方法将 ByteBuffer 加载到 Page Cache 中9. force 刷盘 1. 前言 上一篇文章我们介绍了 HeapByteBuffer 的源码&#…...

【Docker系列】容器内目录显示异常的解决之道

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

echarts:dataZoom属性横向滚动条拖拽不生效

问&#xff1a; 拖拽的过程中&#xff0c;第一次向右拖拽正常&#xff0c;然后就报错&#xff1a; echarts报错&#xff1a; var pointerOption pointerShapeBuilder[axisPointerType](axis,pixeValue,otherExtent),(axis,pixeValue,otherExtent)下划线红色报错&#xff1a;…...

25/1/12 算法笔记 剖析Yolov8底层逻辑

YOLOv8 是一种基于深度学习的目标检测和图像分割模型&#xff0c;属于 YOLO&#xff08;You Only Look Once&#xff09;系列的最新版本。YOLO 系列模型以其高效的实时目标检测能力而闻名&#xff0c;YOLOv8 在此基础上进行了一些优化和改进。 Yolov8的主要特点&#xff1a; …...

Python双指针

双指针 双指针&#xff1a;在区间操作时&#xff0c;利用两个下标同时遍历&#xff0c;进行高效操作 双指针利用区间性质可以把 O ( n 2 ) O(n^2) O(n2) 时间降低到 O ( n ) O(n) O(n) 反向扫描 反向扫描&#xff1a; l e f t left left 起点&#xff0c;不断往右走&…...

1、docker概念和基本使用命令

docker概念 微服务&#xff1a;不再是以完整的物理机为基础的服务软件&#xff0c;而是借助于宿主机的性能。以小量的形式&#xff0c;单独部署的应用。 docker&#xff1a;是一个开源的应用容器引擎&#xff0c;基于go语言开发的&#xff0c;使用时apache2.0的协议。docker是…...

数据结构与算法之链表: LeetCode 92. 反转链表 II (Ts版)

反转链表 II https://leetcode.cn/problems/reverse-linked-list-ii/description/ 描述 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 示例 1 输入&…...

【PPTist】插入形状、插入图片、插入图表

一、插入形状 插入形状有两种情况&#xff0c;一种是插入固定的形状&#xff0c; 一种是插入自定义的形状。 插入固定的形状时&#xff0c;跟上一篇文章 绘制文本框 是一样一样的&#xff0c;都是调用的 mainStore.setCreatingElement() 方法&#xff0c;只不多传的类型不一…...

三台Centos7.9中Docker部署Redis集群

Docker部署Redis集群 1. 安装 Docker 和 Docker Compose安装 Docker&#xff1a;安装 Docker Compose&#xff1a; 2. 配置 Redis 容器和网络3. 启动 Redis 容器4. 设置 Redis 集群4.1 集群创建异常处理 5. 验证和测试总结 如果 CentOS 服务器上还没有安装 Docker 和 Docker Co…...

Entity 的材质(棋盘、条纹、网格)

Entity 的材质 普通物体的材质 import { nextTick, onMounted, ref } from vue import * as Cesium from cesium // console.log(Cesium, Cesium)const viewer ref<any>(null)onMounted(() > { ... })let material Cesium.Color.YELLOW.withAlpha(0.5)Cesium.Colo…...

MACPA:fMRI连接性分析的新工具

摘要 不同脑区的共同激活为它们之间的功能交互或连接提供了一个有价值的衡量指标。元分析连接模型(MACM)是一种经过充分验证的研究某一特定区域共激活模式的方法&#xff0c;该方法对基于任务的功能磁共振成像(task-fMRI)数据进行种子点(seed-based)元分析。虽然MACM是一种强大…...

JavaScript-一份你的前端入门说明书(计算机专业)

一.简介 1.起源 JavaScript 起源于 1995 年,当时它主要是为了满足网页交互的需求而被创建。它最初的设计目的是为了让网页开发者能够在网页中添加一些简单的交互效果和动态内容。在那个时期,网页大多是静态的,而 JavaScript 的出现为网页带来了新的活力。Netscape 公司的 B…...

STM32供电参考设计

STM32供电参考设计 ​ 在图中有VDD&#xff0c;VSS和VDDA&#xff0c;VSSA两种类型的供电引脚&#xff0c;其数据手册解释如下&#xff1a; ​ 令我不解的是&#xff1a;VDDA和VSSA必须分别连接到VDD和VSS&#xff0c;这是什么意思&#xff1f;有大佬能够解答一下吗&#xff1f…...

python+fpdf:创建pdf并实现表格数据写入

目录 创建pdf文件对象 新增页 添加自定义字体 设置字体 设置文字颜色和背景色 插入内容 换行 插入图片 保存pdf 完整代码 安装&#xff1a;pip install fpdf 创建pdf文件对象 from fpdf import FPDF, Alignpdf FPDF() # 创建pdf文件对象 获取边距 print(pdf.l_…...

亚远景-ASPICE评估:汽车软件项目的过程能力评价

ASPICE&#xff08;Automotive SPICE&#xff09;的评估对象主要是汽车软件研发过程。 这个评估过程不仅仅关注最终的软件产品&#xff0c;而是深入到软件开发的全生命周期中&#xff0c;从需求分析、设计、编码、测试到发布和维护等各个环节。 具体来说&#xff0c;ASPICE评…...

电脑提示directx错误导致玩不了游戏怎么办?dx出错的解决方法

想必大家都有过这样的崩溃瞬间&#xff1a;满心欢喜打开心仪的游戏&#xff0c;准备在虚拟世界里大杀四方或者畅游冒险&#xff0c;结果屏幕上突然弹出个 DirectX 错误的提示框&#xff0c;紧接着游戏闪退&#xff0c;一切美好戛然而止。DirectX 作为 Windows 系统下游戏运行的…...

【13】制作镜像以及重启实例

制作镜像 k8s集群 有两个镜像需要制作&#xff0c;一个是master节点&#xff0c;一个是node节点。 在master节点上成功部署了k8s的控制平面&#xff0c;在node节点上部署了worker节点的配置&#xff0c;不知道打包镜像重启之后集群的状态是什么样的。 确认集群在运行&#…...

electron 启动警告

1. 问题 当启动 electron 时&#xff0c;控制台警告 Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security 2. 解决方法 在主进程文件 main.js 中添加如下内容 process.env["ELECTRON_DISABLE_SECURI…...

wow-agent 学习笔记

wow-agent-课程详情 | Datawhale 前两课比较基础&#xff0c;无笔记 第三课 阅卷智能体这一块&#xff0c;曾经做过一点和AI助教相关的内容&#xff0c;也是用了一个prompt去进行CoT&#xff0c;但是风格和课程中的不太相同&#xff0c;在下面附上我的prompt 你是一名资深教…...

使用Cilium/eBPF实现大规模云原生网络和安全

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 目录 抽象 1 Trip.com 云基础设施 1.1 分层架构 1.2 更多细节 2 纤毛在 Trip.com 2.1 推出时间表 2.2 自定义 2.3 优化和调整 2.3.1 解耦安装 2.3.2 避免重试/重启风暴 2.3.3 稳定性优先 2…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

【Java学习笔记】Arrays类

Arrays 类 1. 导入包&#xff1a;import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序&#xff08;自然排序和定制排序&#xff09;Arrays.binarySearch()通过二分搜索法进行查找&#xff08;前提&#xff1a;数组是…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

Leetcode 3577. Count the Number of Computer Unlocking Permutations

Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接&#xff1a;3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯&#xff0c;要想要能够将所有的电脑解锁&#x…...

在Ubuntu中设置开机自动运行(sudo)指令的指南

在Ubuntu系统中&#xff0c;有时需要在系统启动时自动执行某些命令&#xff0c;特别是需要 sudo权限的指令。为了实现这一功能&#xff0c;可以使用多种方法&#xff0c;包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法&#xff0c;并提供…...

【单片机期末】单片机系统设计

主要内容&#xff1a;系统状态机&#xff0c;系统时基&#xff0c;系统需求分析&#xff0c;系统构建&#xff0c;系统状态流图 一、题目要求 二、绘制系统状态流图 题目&#xff1a;根据上述描述绘制系统状态流图&#xff0c;注明状态转移条件及方向。 三、利用定时器产生时…...

《基于Apache Flink的流处理》笔记

思维导图 1-3 章 4-7章 8-11 章 参考资料 源码&#xff1a; https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分

一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计&#xff0c;提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合&#xff1a;各模块职责清晰&#xff0c;便于独立开发…...

uniapp 字符包含的相关方法

在uniapp中&#xff0c;如果你想检查一个字符串是否包含另一个子字符串&#xff0c;你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的&#xff0c;但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...

springboot 日志类切面,接口成功记录日志,失败不记录

springboot 日志类切面&#xff0c;接口成功记录日志&#xff0c;失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...