JDK核心JAVA源码解析(5) - JAVA File MMAP原理解析(上)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: JDK核心JAVA源码解析(5) - JAVA File MMAP原理解析(上)

想写这个系列很久了,对自己也是个总结与提高。原来在学JAVA时,那些JAVA入门书籍会告诉你一些规律还有法则,但是用的时候我们一般很难想起来,因为我们用的少并且不知道为什么。知其所以然方能印象深刻并学以致用。

本篇文章针对JAVA中的MMAP的文件映射读写机制,来分析为何很多告诉框架用了这个机制,以及这个机制好在哪里,快在哪里。

本文基于JDK 1.8


JAVA File MMAP原理解析


1. 内存管理术语

  • MMC:CPU的内存管理单元。
  • 物理内存:即内存条的内存空间。
  • 虚拟内存:计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
  • 页面文件:操作系统反映构建并使用虚拟内存的硬盘空间大小而创建的文件,在windows下,即pagefile.sys文件,其存在意味着物理内存被占满后,将暂时不用的数据移动到硬盘上。
  • 缺页中断:当程序试图访问已映射在虚拟地址空间中但未被加载至物理内存的一个分页时,由MMC发出的中断。如果操作系统判断此次访问是有效的,则尝试将相关的页从虚拟内存文件中载入物理内存。


2. 什么是MMAP


尽管从JDK 1.4版本开始,Java内存映射文件(Memory Mapped Files)就已经在java.nio包中,但它对很多程序开发者来说仍然是一个相当新的概念。引入NIO后,Java IO已经相当快,而且内存映射文件提供了Java有可能达到的最快IO操作,这也是为什么那些高性能Java应用应该使用内存映射文件来持久化数据。 作为NIO的一个重要的功能,Mmap方法为我们提供了将文件的部分或全部映射到内存地址空间的能力,同当这块内存区域被写入数据之后会变成脏页,操作系统会用一定的算法把这些数据写入到文件中,而我们的java程序不需要去关心这些。这就是内存映射文件的一个关键优势,即使你的程序在刚刚写入内存后就挂了,操作系统仍然会将内存中的数据写入文件系统。 另外一个更突出的优势是共享内存,内存映射文件可以被多个进程同时访问,起到一种低时延共享内存的作用。


3. Java MMAP实现


3.1. Java MMAP 与 FileChannel操作文件对比

package com.github.hashZhang.scanfold.jdk.file;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Random;
public class FileMmapTest {
    public static void main(String[] args) throws Exception {
        //记录开始时间
        long start = System.currentTimeMillis();
        //通过RandomAccessFile的方式获取文件的Channel,这种方式针对随机读写的文件较为常用,我们用文件一般是随机读写
        RandomAccessFile randomAccessFile = new RandomAccessFile("./FileMmapTest.txt", "rw");
        FileChannel channel = randomAccessFile.getChannel();
        System.out.println("FileChannel初始化时间:" + (System.currentTimeMillis() - start) + "ms");
        //内存映射文件,模式是READ_WRITE,如果文件不存在,就会被创建
        MappedByteBuffer mappedByteBuffer1 = channel.map(FileChannel.MapMode.READ_WRITE, 0, 128 * 1024 * 1024);
        MappedByteBuffer mappedByteBuffer2 = channel.map(FileChannel.MapMode.READ_WRITE, 0, 128 * 1024 * 1024);
        System.out.println("MMAPFile初始化时间:" + (System.currentTimeMillis() - start) + "ms");
        start = System.currentTimeMillis();
        testFileChannelSequentialRW(channel);
        System.out.println("FileChannel顺序读写时间:" + (System.currentTimeMillis() - start) + "ms");
        start = System.currentTimeMillis();
        testFileMMapSequentialRW(mappedByteBuffer1, mappedByteBuffer2);
        System.out.println("MMAPFile顺序读写时间:" + (System.currentTimeMillis() - start) + "ms");
        start = System.currentTimeMillis();
        try {
            testFileChannelRandomRW(channel);
            System.out.println("FileChannel随机读写时间:" + (System.currentTimeMillis() - start) + "ms");
        } finally {
            randomAccessFile.close();
        }
        //文件关闭不影响MMAP写入和读取
        start = System.currentTimeMillis();
        testFileMMapRandomRW(mappedByteBuffer1, mappedByteBuffer2);
        System.out.println("MMAPFile随机读写时间:" + (System.currentTimeMillis() - start) + "ms");
    }
    public static void testFileChannelSequentialRW(FileChannel fileChannel) throws Exception {
            byte[] bytes = "测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1".getBytes();
            byte[] to = new byte[bytes.length];
            //分配直接内存,减少复制
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length);
            //顺序写入
            for (int i = 0; i < 100000; i++) {
                byteBuffer.put(bytes);
                byteBuffer.flip();
                fileChannel.write(byteBuffer);
                byteBuffer.flip();
            }
            fileChannel.position(0);
            //顺序读取
            for (int i = 0; i < 100000; i++) {
                fileChannel.read(byteBuffer);
                byteBuffer.flip();
                byteBuffer.get(to);
                byteBuffer.flip();
            }
    }
    public static void testFileMMapSequentialRW(MappedByteBuffer mappedByteBuffer1, MappedByteBuffer mappedByteBuffer2) throws Exception {
        byte[] bytes = "测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2".getBytes();
        byte[] to = new byte[bytes.length];
        //顺序写入
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer1.put(bytes);
        }
        //顺序读取
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer2.get(to);
        }
    }
    public static void testFileChannelRandomRW(FileChannel fileChannel) throws Exception {
        try {
            byte[] bytes = "测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1测试字符串1".getBytes();
            byte[] to = new byte[bytes.length];
            //分配直接内存,减少复制
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length);
            //随机写入
            for (int i = 0; i < 100000; i++) {
                byteBuffer.put(bytes);
                byteBuffer.flip();
                fileChannel.position(new Random(i).nextInt(bytes.length*100000));
                fileChannel.write(byteBuffer);
                byteBuffer.flip();
            }
            //随机读取
            for (int i = 0; i < 100000; i++) {
                fileChannel.position(new Random(i).nextInt(bytes.length*100000));
                fileChannel.read(byteBuffer);
                byteBuffer.flip();
                byteBuffer.get(to);
                byteBuffer.flip();
            }
        } finally {
            fileChannel.close();
        }
    }
    public static void testFileMMapRandomRW(MappedByteBuffer mappedByteBuffer1, MappedByteBuffer mappedByteBuffer2) throws Exception {
        byte[] bytes = "测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2测试字符串2".getBytes();
        byte[] to = new byte[bytes.length];
        //随机写入
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer1.position(new Random(i).nextInt(bytes.length*100000));
            mappedByteBuffer1.put(bytes);
        }
        //随机读取
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer2.position(new Random(i).nextInt(bytes.length*100000));
            mappedByteBuffer2.get(to);
        }
    }
}


在这里,我们初始化了一个文件,并把它映射到了128M的内存中。分FileChannel还有MMAP的方式,通过顺序或随机读写,写了一些内容并读取一部分内容。



运行结果是:

FileChannel初始化时间:7ms
MMAPFile初始化时间:8ms
FileChannel顺序读写时间:420ms
MMAPFile顺序读写时间:20ms
FileChannel随机读写时间:860ms
MMAPFile随机读写时间:45ms

可以看到,通过MMAP内存映射文件的方式操作文件,更加快速,并且性能提升的相当明显。


3.2. Java MMAP 源代码分析

我们可以利用strace命令先看看上面程序的系统调用:

strace -c java com/github/hashZhang/scanfold/jdk/file/FileMmapTest


结果如下:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.79    0.410139      205070         2           futex
  0.04    0.000151          13        12           mprotect
  0.03    0.000127           6        23           mmap
  0.03    0.000110           5        23        14 open
  0.02    0.000065          22         3           munmap
  0.01    0.000058           7         8           read
  0.01    0.000053           6         9           close
  0.01    0.000052           6         9           fstat
  0.01    0.000040           4        10         7 stat
  0.01    0.000039          10         4           brk
  0.01    0.000036          36         1           clone
  0.01    0.000033          11         3         2 access
  0.01    0.000025          13         2           readlink
  0.01    0.000024          12         2           rt_sigaction
  0.00    0.000012          12         1           getrlimit
  0.00    0.000012          12         1           set_tid_address
  0.00    0.000011          11         1           rt_sigprocmask
  0.00    0.000011          11         1           set_robust_list
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.410998                   117        23 total

我们可以注意到有open、mmap、munmap、close这几个比较重要的系统调用,我们的文件操作基本主要和这几个系统调用有关。

接下来我们来看MMAP相关的源代码:


3.2.1. 初始化内存映射文件Buffer -> MappedByteBuffer

MappedByteBuffer的类关系:


可以看到,DirectByteBuffer是一种MappedByteBuffer。

channel.map(FileChannel.MapMode.READ_WRITE, 0, 128 * 1024 * 1024);对应的源代码:

FileChannel.java:

public MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)
            throws IOException
{
    //保证文件还没被关闭,基本上FileChannel的每个方法都会做这个判断
    ensureOpen();
    //模式不能为空
    if (mode == null)
        throw new NullPointerException("Mode is null");
    if (position < 0L)
        throw new IllegalArgumentException("Negative position");
    if (size < 0L)
        throw new IllegalArgumentException("Negative size");
    //不能超过长度限制
    if (position + size < 0)
        throw new IllegalArgumentException("Position + size overflow");
    //size不能超过Integer的最大值
    //因为在写入数据时,java地址转换为linux内存地址的时候,强制转换成了int类型,所以映射大小不能超过Integer的最大值,也就是<2G(2^31-1)
    if (size > Integer.MAX_VALUE)
        throw new IllegalArgumentException("Size exceeds Integer.MAX_VALUE");
    int imode = -1;
    if (mode == FileChannel.MapMode.READ_ONLY)
        imode = MAP_RO;
    else if (mode == FileChannel.MapMode.READ_WRITE)
        imode = MAP_RW;
    else if (mode == FileChannel.MapMode.PRIVATE)
        imode = MAP_PV;
    assert (imode >= 0);
    if ((mode != FileChannel.MapMode.READ_ONLY) && !writable)
        throw new NonWritableChannelException();
    if (!readable)
        throw new NonReadableChannelException();
    long addr = -1;
    int ti = -1;
    try {
        //线程锁控制,这里用的原生线程锁,限制最多同时有两个线程(进程)同时去映射同一个文件
        begin();
        ti = threads.add();
        if (!isOpen())
            return null;
        //首先要获取文件目前的大小,来判断是否需要扩展文件
        long filesize;
        do {
            //JNI调用1:调用fstat命令获取文件大小
            filesize = nd.size(fd);
        } while (
            //遇到系统EINTR信号(被中断的系统调用)时,要一直重试,因为这不代表调用有错误
            (filesize == IOStatus.INTERRUPTED)
            && isOpen()
        );
        if (!isOpen())
            return null;
        //当映射的文件位置与大小超过文件整体大小时,需要扩展文件
        if (filesize < position + size) { // Extend file size
            if (!writable) {
                throw new IOException("Channel not open for writing " +
                        "- cannot extend file to required size");
            }
            int rv;
            do {
                //JNI调用2: 调用ftruncate扩展文件
                rv = nd.truncate(fd, position + size);
            } while (
                //遇到系统EINTR信号(被中断的系统调用)时,要一直重试,因为这不代表调用有错误
                (rv == IOStatus.INTERRUPTED) && isOpen()
            );
            if (!isOpen())
                return null;
        }
        //如果映射size为0,直接返回空的DirectByteBuffer或者只读的DirectByteBufferR
        //个人感觉这个可以提前,放到判断文件大小之前,但是更严谨的话,应该放到现在的位置
        if (size == 0) {
            addr = 0;
            // a valid file descriptor is not required
            FileDescriptor dummy = new FileDescriptor();
            if ((!writable) || (imode == MAP_RO))
                return Util.newMappedByteBufferR(0, 0, dummy, null);
            else
                return Util.newMappedByteBuffer(0, 0, dummy, null);
        }
        //计算出页位置, allocationGranularity是系统文件分页大小(pageCache的page大小)
        int pagePosition = (int)(position % allocationGranularity);
        //计算出映射起始页位置
        long mapPosition = position - pagePosition;
        //计算需要的大小
        long mapSize = size + pagePosition;
        try {
            //JNI调用3: mmap
            addr = map0(imode, mapPosition, mapSize);
        } catch (OutOfMemoryError x) {
            // 如果因为内存不足导致的失败,尝试请求Full-GC
            System.gc();
            try {
                //因为System.gc()只是告诉jvm要做FullGC,但是不一定立刻做,所以这里等待100ms来让JVM FullGC(FullGC时间不算在这个100ms内,因为FullGC是全局中断)
                Thread.sleep(100);
            } catch (InterruptedException y) {
                Thread.currentThread().interrupt();
            }
            try {
                //重新尝试
                addr = map0(imode, mapPosition, mapSize);
            } catch (OutOfMemoryError y) {
                //FullGC之后还是没能分配,就抛异常
                throw new IOException("Map failed", y);
            }
        }
        // On Windows, and potentially other platforms, we need an open
        // file descriptor for some mapping operations.
        FileDescriptor mfd;
        try {
            mfd = nd.duplicateForMapping(fd);
        } catch (IOException ioe) {
            unmap0(addr, mapSize);
            throw ioe;
        }
        assert (IOStatus.checkAll(addr));
        assert (addr % allocationGranularity == 0);
        int isize = (int)size;
        //新建一个Unmapper来在GC的时候回收掉mmap出来的内存
        //这个回收也是jni调用munmap
        FileChannelImpl.Unmapper um = new FileChannelImpl.Unmapper(addr, mapSize, isize, mfd);
        if ((!writable) || (imode == MAP_RO)) {
            //返回只读的DirectByteBuffer封装的mmap内存
            return Util.newMappedByteBufferR(isize,
                    addr + pagePosition,
                    mfd,
                    um);
        } else {
            //返回DirectByteBuffer封装的mmap内存
            return Util.newMappedByteBuffer(isize,
                    addr + pagePosition,
                    mfd,
                    um);
        }
    } finally {
        threads.remove(ti);
        end(IOStatus.checkAll(addr));
    }
}


相关文章
|
3天前
|
存储 Java 计算机视觉
Java二维数组的使用技巧与实例解析
本文详细介绍了Java中二维数组的使用方法
25 15
|
22天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
105 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
3天前
|
算法 搜索推荐 Java
【潜意识Java】深度解析黑马项目《苍穹外卖》与蓝桥杯算法的结合问题
本文探讨了如何将算法学习与实际项目相结合,以提升编程竞赛中的解题能力。通过《苍穹外卖》项目,介绍了订单配送路径规划(基于动态规划解决旅行商问题)和商品推荐系统(基于贪心算法)。这些实例不仅展示了算法在实际业务中的应用,还帮助读者更好地准备蓝桥杯等编程竞赛。结合具体代码实现和解析,文章详细说明了如何运用算法优化项目功能,提高解决问题的能力。
32 6
|
3天前
|
存储 算法 搜索推荐
【潜意识Java】期末考试可能考的高质量大题及答案解析
Java 期末考试大题整理:设计一个学生信息管理系统,涵盖面向对象编程、集合类、文件操作、异常处理和多线程等知识点。系统功能包括添加、查询、删除、显示所有学生信息、按成绩排序及文件存储。通过本题,考生可以巩固 Java 基础知识并掌握综合应用技能。代码解析详细,适合复习备考。
12 4
|
9天前
|
SQL Java 数据库连接
如何在 Java 代码中使用 JSqlParser 解析复杂的 SQL 语句?
大家好,我是 V 哥。JSqlParser 是一个用于解析 SQL 语句的 Java 库,可将 SQL 解析为 Java 对象树,支持多种 SQL 类型(如 `SELECT`、`INSERT` 等)。它适用于 SQL 分析、修改、生成和验证等场景。通过 Maven 或 Gradle 安装后,可以方便地在 Java 代码中使用。
106 11
|
3天前
|
存储 Java
【潜意识Java】期末考试可能考的选择题(附带答案解析)
本文整理了 Java 期末考试中常见的选择题,涵盖数据类型、控制结构、面向对象编程、集合框架、异常处理、方法、流程控制和字符串等知识点。每道题目附有详细解析,帮助考生巩固基础,加深理解。通过这些练习,考生可以更好地准备考试,掌握 Java 的核心概念和语法。
12 1
|
8天前
|
存储 分布式计算 Hadoop
基于Java的Hadoop文件处理系统:高效分布式数据解析与存储
本文介绍了如何借鉴Hadoop的设计思想,使用Java实现其核心功能MapReduce,解决海量数据处理问题。通过类比图书馆管理系统,详细解释了Hadoop的两大组件:HDFS(分布式文件系统)和MapReduce(分布式计算模型)。具体实现了单词统计任务,并扩展支持CSV和JSON格式的数据解析。为了提升性能,引入了Combiner减少中间数据传输,以及自定义Partitioner解决数据倾斜问题。最后总结了Hadoop在大数据处理中的重要性,鼓励Java开发者学习Hadoop以拓展技术边界。
34 7
|
15天前
|
监控 JavaScript 数据可视化
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
|
3天前
|
Java 编译器 程序员
【潜意识Java】期末考试可能考的简答题及答案解析
为了帮助同学们更好地准备 Java 期末考试,本文列举了一些常见的简答题,并附上详细的答案解析。内容包括类与对象的区别、多态的实现、异常处理、接口与抽象类的区别以及垃圾回收机制。通过这些题目,同学们可以深入理解 Java 的核心概念,从而在考试中更加得心应手。每道题都配有代码示例和详细解释,帮助大家巩固知识点。希望这些内容能助力大家顺利通过考试!
|
27天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。

推荐镜像

更多