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

【源码解析】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; // 1GB

        try {
            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; // 1GB

        long 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-private
                 FileDescriptor 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 或者容量为 0
    if ((address == 0) || (capacity() == 0))
        // 表示已经不在物理内存里面了
        return true;
    // 获取 mmap 分配的内存的起始位置,也就是图中的 mappingOffset
    long 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 里面的数据进行刷盘。





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


http://www.kler.cn/a/500576.html

相关文章:

  • Ubuntu中使用miniconda安装R和R包devtools
  • RabbitMQ高级篇
  • nvim 打造成可用的IDE(2)
  • MySQL存储引擎、索引、索引失效
  • Unity + Firebase + GoogleSignIn 导入问题
  • ECharts饼图下钻
  • 一文详解YOLOv8多模态目标检测(可见光+红外图像,基于Ultralytics官方代码实现),轻松入门多模态检测领域!
  • 金融项目实战 03|JMeter脚本实现手工接口测试
  • 【微信小程序】回到顶部图标-页面滚动事件 | 漫画-综合实训
  • 深度学习中的EMA技术:原理、实现与实验分析
  • 计算机网络之---传输层的功能
  • conntrack iptables 安全组
  • Oracle 使用dbms_stats.gather_table_stats来进行表analyse,收集表统计信息
  • 《零基础Go语言算法实战》【题目 2-7】defer 关键字特性
  • spring boot 支持jsonp请求
  • 阿里云发现后门webshell,怎么处理,怎么解决?
  • React - router的使用 结合react-redux的路由守卫
  • 依赖网络系统混合级联故障下系统可靠性提高与弹性的组合优化
  • 网络安全 | Web安全常见漏洞和防护经验策略
  • 苍穹外卖及软件开发介绍
  • 基于 B2C 的网上拍卖系统:秒杀与竞价功能的实现
  • ip归属地和手机号是一个地址吗
  • 【微服务】面试 2、负载均衡
  • matlab专栏-模拟滤波器设计
  • Spring——几个常用注解
  • linux服务器安装mysql数据库和nginx