JDK核心JAVA源码解析 - JAVA File MMAP原理解析

Posted 干货满满张哈希

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK核心JAVA源码解析 - 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));
    

1.JNI调用1:调用fstat命令获取文件大小:

FileDispatcherImpl.c:

#define fstat64 fstat
Java_sun_nio_ch_FileDispatcherImpl_size0(JNIEnv *env, jobject this, jobject fdo)

    struct stat64 fbuf;

    if (fstat64(fdval(env, fdo), &fbuf) < 0)
        return handle(env, -1, "Size failed");
    return fbuf.st_size;

fstat函数:

定义函数: int fstat(int fildes, struct stat *buf);

函数说明: fstat()用来将参数fildes 所指的文件状态, 复制到参数buf 所指的结构中(struct stat). Fstat()与stat()作用完全相同, 不同处在于传入的参数为已打开的文件描述词. 详细内容请参考stat().

返回值: 执行成功则返回0, 失败返回-1, 错误代码存于errno.

对于返回值如果为EINTR的处理如下:
FileDispatcherImpl.c:

static jlong
handle(JNIEnv *env, jlong rv, char *msg)

    if (rv >= 0)
        return rv;
    if (errno == EINTR)
        return IOS_INTERRUPTED;
    JNU_ThrowIOExceptionWithLastError(env, msg);
    return IOS_THROWN;

2.JNI调用2:调用ftruncate扩展文件:

FileDispatcherImpl.c:

#define ftruncate64 ftruncate
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_truncate0(JNIEnv *env, jobject this,
                                             jobject fdo, jlong size)

    return handle(env,
                  ftruncate64(fdval(env, fdo), size),
                  "Truncation failed");

ftruncate函数:

定义函数: int ftruncate(int fd, off_t length);

函数说明: ftruncate()会将参数fd 指定的文件大小改为参数length 指定的大小。参数fd 为已打开的文件描述词,而且必须是以写入模式打开的文件。如果原来的文件大小比参数length 大,则超过的部分会被删去。

返回值: 执行成功则返回0, 失败返回-1, 错误原因存于errno.

错误代码:
1、EBADF 参数fd 文件描述词为无效的或该文件已关闭。
2、EINVAL 参数fd 为一socket 并非文件, 或是该文件并非以写入模式打开。

3. JNI调用3:调用mmap构建内存映射

FileDispatcherImpl.c:

#define mmap64 mmap
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
                                     jint prot, jlong off, jlong len)

    void *mapAddress = 0;
    jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
    jint fd = fdval(env, fdo);
    int protections = 0;
    int flags = 0;

    if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) 
        protections = PROT_READ;
        flags = MAP_SHARED;
     else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) 
        protections = PROT_WRITE | PROT_READ;
        flags = MAP_SHARED;
     else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) 
        protections =  PROT_WRITE | PROT_READ;
        flags = MAP_PRIVATE;
    
    //调用mmap
    mapAddress = mmap64(
        0,                    /* Let OS decide location */
        len,                  /* Number of bytes to map */
        protections,          /* File permissions */
        flags,                /* Changes are shared */
        fd,                   /* File descriptor of mapped file */
        off);                 /* Offset into file */
    //内存不足时,抛出OutOfMemoryError
    if (mapAddress == MAP_FAILED) 
        if (errno == ENOMEM) 
            JNU_ThrowOutOfMemoryError(env, "Map failed");
            return IOS_THROWN;
        
        return handle(env, -1, "Map failed");
    

    return ((jlong) (unsigned long) mapAddress);

mmap函数:

定义函数: void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize);

函数说明: mmap()用来将某个文件内容映射到内存中,对该内存区域的存取即是直接对该文件内容的读写。

参数说明:

参数说明
start指向欲对应的内存起始地址,通常设为NULL,代表让系统自动选定地址,对应成功后该地址会返回。
length代表将文件中多大的部分对应到内存。
prot代表映射区域的保护方式,有下列组合:PROT_EXEC:映射区域可被执行;PROT_READ:映射区域可被读取;PROT_WRITE:映射区域可被写入;PROT_NONE:映射区域不能存取。
flags会影响映射区域的各种特性:MAP_FIXED:如果参数 start 所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。MAP_SHARED:对应射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。MAP_PRIVATE:对应射区域的写入操作会产生一个映射文件的复制,即私人的”写入时复制” (copy on write)对此区域作的任何修改都不会写回原来的文件内容。MAP_ANONYMOUS:建立匿名映射,此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。MAP_DENYWRITE:只允许对应射区域的写入操作,其他对文件直接写入的操作将会被拒绝。MAP_LOCKED:将映射区域锁定住,这表示该区域不会被置换(swap)。

在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
fd open()返回的文件描述词,代表欲映射到内存的文件。
offset 文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。

错误代码:
- EBADF 参数fd 不是有效的文件描述词。
- EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED 则要有PROT_WRITE 以及该文件要能写入。
- EINVAL 参数start、length 或offset 有一个不合法。
- EAGAIN 文件被锁住,或是有太多内存被锁住。
- ENOMEM 内存不足。

3.2.1.1. Q1. 内存映射文件占用的是哪里的内存?

通过上面的源码分析,我们可以看到返回的是DirectByteBuffer,但是这个DirectByteBuffer并没有占用到JVM的-XX:MaxDirectMemorySize的空间。我们可以从两个地方看出来:

  1. DirectByteBuffer的构造方法与其他的构造方法不一样,调用的是:protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper),而且是通过反射,所以没走到sun对于DirectMemory使用的统计中
  2. 我们可以在代码中添加查看直接内存占用的代码来看是否有占用,这个参考我的系列上一篇文章JDK核心JAVA源码解析(4) - 堆外内存、零拷贝、DirectByteBuffer以及针对于NIO中的FileChannel的思考
    这个mmap占用的内存是一块独立于JVM之外的,可以进程间共享的内存。
3.2.1.2. Q2. 内存映射文件的内存什么时候会被回收?

首先,munmap并没有被JDK开放。我们想主动回收是无法做到的(当然也可以通过反射调用,但是并不推荐)。这个不开放也可以理解,因为用户如果主动调用,会导致GC DirectBuffer的时候,报出内存访问异常导致JVM崩溃(如果用户调用了munmap,对应的MappedByteBuffer被GC时,会在被调用一次。这时如果内存已经被其他程序占用,会报一个内存访问异常)。

我们要想解除映射只能先把buffer置为null,然后祈祷GC赶紧起作用,实在等不及还可以用System.gc()催促一下GC赶快干活,不过后果是会引发FullGC。

所以,在写完当前文件块,需要映射下一块文件时,我们一般就把对应的MappedByteBuffer设置为null就行了,然后继续map就行了,因为在之前的源代码讲解中,我们看到,如果内存不足,会调用System.gc()

3.2.2. 对MappedByteBuffer进行读写

对于MappedByteBuffer的读写和对于ByteBuffer的读写是一样的,可以参考我的另一片文章:https://blog.csdn.net/zhxdick/article/details/51167313

3.2.2.1 对MappedByteBuffer进行读写,为何最大只能2GB-1B

我们来看底层实现:对于所有DirectByteBuffer的读写,都用到了Unsafe类的public native void putByte(Object o, long offset, byte x);方法,底层实现是:

unsafe.cpp:

UNSAFE_ENTRY(void, Unsafe_SetNative##Type(JNIEnv *env, jobject unsafe, jlong addr, java_type x)) \\
  UnsafeWrapper("Unsafe_SetNative"#Type); \\
  JavaThread* t = JavaThread::current(); \\
  t->set_doing_unsafe_access(true); \\
  //获取地址
  void* p = addr_from_java(addr); \\
  //设置值
  *(volatile native_type*)p = x; \\
  t->set_doing_unsafe_access(false); \\
UNSAFE_END \\

那么这个获取地址的方法是啥样子呢?

inline void* addr_from_java(jlong addr) 
  // This assert fails in a variety of ways on 32-bit systems.
  // It is impossible to predict whether native code that converts
  // pointers to longs will sign-extend or zero-extend the addresses.
  //assert(addr == (uintptr_t)addr, "must not be odd high bits");
  //转换为int
  return (void*)(uintptr_t)addr;

这里我们看到,转换地址会被强制转换为int类型,所以只能映射2GB-1B。

但是为何-XX:MaxDirectMemory可以指定比2G大的值呢?因为对于分配的直接内存中的buffer,有对一个BitMap管理他们的基址,可以保证映射出对的地址,类似于堆内存的基址映射。但是对于文件映射内存,JVM没办法维护这么一个基址,或者说没必要(大于2GB-1B我们多映射两次自己维护就行了)。

3.2.2.2. 文件映射内存是如何更新到硬盘文件的?

对于我们的程序,只需要修改MappedByteBuffer就可以了,之后操作系统根据优先搜索树的算法,通过pdflush进程刷入磁盘。
就算我们的程序挂了,操作系统也会把这部分内存的脏页刷入磁盘。
但是如果系统挂了,重启等,这部分数据会丢失。

那我们有强制刷入磁盘的方法么?linux对应的系统调用是msync()函数(参考:http://man7.org/linux/man-pages/man2/msync.2.html)。对应的Java方法是MappedByteBuffer.force(),不过使用这个方法会大幅度降低效率,慎用!

MappedByteBuffer.java:

public final MappedByteBuffer force() 
    checkMapped();
    if ((address != 0) && (capacity() != 0)) 
        long offset = mappingOffset();
        //原生调用force0
        force0(fd, mappingAddress(offset), mappingLength(offset));
    
    return this;

MappedByteBuffer.c:

JNIEXPORT void JNICALL
Java_java_nio_MappedByteBuffer_force0(JNIEnv *env, jobject obj, jobject fdo,
                                      jlong address, jlong len)

    void* a = (void *)jlong_to_ptr(address);
    //调用msync
    int result = msync(a, (size_t)len, MS_SYNC);
    if (result == -1) 
        JNU_ThrowIOExceptionWithLastError(env, "msync failed");
    
3.2.2.3. 如何查看文件映射脏页,如何统计MMAP的内存大小?

我们写一个测试程序:

public static void main(String[] args) throws Exception 
    RandomAccessFile randomAccessFile = new RandomAccessFile("./FileMmapTest.txt", "rw");
    FileChannel channel = randomAccessFile.getChannel();
    MappedByteBuffer []mappedByteBuffers = new MappedByteBuffer[5];

    //开5个相同文件的MappedByteBuffer,但是实际机器内存只有8G
    mappedByteBuffers[0] = channel.map(FileChannel.MapMode.READ_WRITE, 0, 2 * 1024 * 1024 * 1024 - 1);
    mappedByteBuffers[1] = channel.map(FileChannel.MapMode.READ_WRITE, 0, 2 * 1024 * 1024 * 1024 - 1);
    mappedByteBuffers[2] = channel.map(FileChannel.MapMode.READ_WRITE, 0, 2 * 1024 * 1024 * 1024 - 1);
    mappedByteBuffers[3] = channel.map(FileChannel.MapMode.READ_WRITE, 0, 2 * 1024 * 1024 * 1024 - 1);
    mappedByteBuffers[4] = channel.map(FileChannel.MapMode.READ_WRITE, 0, 2 * 1024 * 1024 * 1024 - 1);


    for (int j = 0; j < 2*1024*1024*1024 - 1; j++) 
        mappedByteBuffers[0].put("a".getBytes());
    
    TimeUnit.SECONDS.sleep(1);
    byte []to = new byte[1];

    for (int j = 0; j < 2*1024*1024*1024 - 1; j++) 
        mappedByteBuffers[1].get(to);
        mappedByteBuffers[2].get(to);
        mappedByteBuffers[3].get(to);
        mappedByteBuffers[4].get(to);
    

    while(true) 
        TimeUnit.SECONDS.sleep(1);
    

等到程序运行到最后的死循环的时候,我们来看top -c的结果:

KiB Mem :  7493092 total,   147876 free,  3891680 used,  3453536 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  2845100 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+                                                                                                                                                                                              
25458 zhangha+  20   0 14.147g 8.840g 8.599g S   0.0  124   2:33.16 java

可以观察到非常有意思的现象,这个进程占用了124%的内存,实际上Swap为0。总占用也没到100%。这是为什么呢?

我们来看下这个进程的smaps文件,这里进程号是25485,我们映射的文件是FileMmapTest.txt:

$ grep -A 11 FileMmapTest.txt  /proc/25458/smaps
7fa870000000-7fa8f0000000 rw-s 00000000 ca:01 25190272                   /home/zhanghaoxin/FileMmapTest.txt
Size:            2097152 kB
Rss:             2097152 kB
Pss:              493463 kB
Shared_Clean:    2097152 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:      2014104 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
--
7fa8f0000000-7fa970000000 rw-s 00000000 ca:01 25190272                   /home/zhanghaoxin/FileMmapTest.txt
Size:            2097152 kB
<

以上是关于JDK核心JAVA源码解析 - JAVA File MMAP原理解析的主要内容,如果未能解决你的问题,请参考以下文章

Java中的容器(集合)之HashMap源码解析

Java Executor源码解析—ThreadPoolExecutor线程池execute核心方法源码一万字

JDK 线程池源码实现解析

从JDK源码看Java域名解析

Java AtomicInteger和AtomicStampedReference源码深度解析

jdk1.8 HashMap扩容原理解析