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

简介: JDK核心JAVA源码解析(5) - JAVA File MMAP原理解析(下)

3.2.2. 对MappedByteBuffer进行读写

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

1167313

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
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
--
7fa970000000-7fa9f0000000 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
--
7fa9f0000000-7faa70000000 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
--
7faa70000000-7faaf0000000 rw-s 00000000 ca:01 25190272                   /home/zhanghaoxin/FileMmapTest.txt
Size:            2097152 kB
Rss:              616496 kB
Pss:              123299 kB
Shared_Clean:     616496 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:       616492 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB

其中比较重要的8个字段的含义分别如下:

  • Size:表示该映射区域在虚拟内存空间中的大小。
  • Rss:表示该映射区域当前在物理内存中占用了多少空间
  • Pss:该虚拟内存区域平摊计算后使用的物理内存大小(有些内存会和其他进程共享,例如mmap进来的)。比如该区域所映射的物理内存部分同时也被另一个进程映射了,且该部分物理内存的大小为1000KB,那么该进程分摊其中一半的内存,即Pss=500KB。
  • Shared_Clean:和其他进程共享的未被改写的page的大小
  • Shared_Dirty: 和其他进程共享的被改写的page的大小
  • Private_Clean:未被改写的私有页面的大小。
  • Private_Dirty: 已被改写的私有页面的大小。
  • Swap:表示非mmap内存(也叫anonymous memory,比如malloc动态分配出来的内存)由于物理内存不足被swap到交换空间的大小。

我们可以看到,把这五个MappedByteBuffer的Pss加起来正好是2097151,就是我们映射的大小。可以推断出,我们这五个MappedByteBuffer在linux中的实现就是对应同一块内存。 同时,top命令看到的内存并不准,top,命令统计的是RSS字段,其实对于MMAP来说,更准确的应该是统计PSS字段


3.2.2.4. pdflush如何配置

在linux操作系统中,写操作是异步的,即写操作返回的时候数据并没有真正写到磁盘上,而是先写到了系统cache里,随后由pdflush内核线程将系统中的脏页写到磁盘上,在下面几种情况下:


  1. 定时方式: 定时机制定时唤醒pdflush内核线程,周期为/proc/sys/vm/dirty_writeback_centisecs ,单位 是(1/100)秒,每次周期性唤醒的pdflush线程并不是回写所有的脏页,而是只回写变脏时间超过 /proc/sys/vm/dirty_expire_centisecs(单位也是1/100秒)。 注意:变脏的时间是以文件的inode节点变脏的时间为基准的,也就是说如果某个inode节点是10秒前变脏的, pdflush就认为这个inode对应的所有脏页的变脏时间都是10秒前,即使可能部分页面真正变脏的时间不到10秒, 细节可以查看内核函数wb_kupdate()。
  2. 内存不足的时候: 这时并不将所有的dirty页写到磁盘,而是每次写大概1024个页面,直到空闲页面满足需求为止。
  3. 写操作时发现脏页超过一定比例: 当脏页占系统内存的比例超过/proc/sys/vm/dirty_background_ratio 的时候,write系统调用会唤醒pdflush回写dirty page,直到脏页比例低于/proc/sys/vm/dirty_background_ratio,但write系统调用不会被阻塞,立即返回。当脏页占系统内存的比例超过/proc/sys/vm/dirty_ratio的时候, write系统调用会被被阻塞,主动回写dirty page,直到脏页比例低于/proc/sys/vm/dirty_ratio,这一点在2.4内核中是没有的。
  4. 用户调用sync系统调用: 这是系统会唤醒pdflush直到所有的脏页都已经写到磁盘为止。


proc下的相关控制参数:

  • /proc/sys/vm/dirty_ratio: 这个参数控制一个进程在文件系统中的文件系统写缓冲区的大小,单位是百分比,表示系统内存的百分比,表示当一个进程中写缓冲使用到系统内存多少的时候,再有磁盘写操作时开始向磁盘写出数据。增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。但是,当你需要持续、恒定的写入场合时,应该降低其数值.一般缺省是 40。设置方法如下:echo 30 >/proc/sys/vm/dirty_ratio
  • /proc/sys/vm/dirty_background_ratio: 这个参数控制文件系统的pdflush进程,在何时刷新磁盘。单位是百分比,表示系统总内存的百分比,意思是当磁盘的脏数据缓冲到系统内存多少的时候,pdflush开始把脏数据刷新到磁盘。增大会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。但是,当你需要持续、恒定的写入场合时,应该降低其数值.一般缺省是10。设置方法如下:echo 8 >/proc/sys/vm/dirty_background_ratio
  • /proc/sys/vm/dirty_writeback_centisecs: Pdflush写后台进程每隔多久被唤醒并执行把脏数据写出到硬盘。单位是 1/100 秒。如果你的系统是持续地写入动作,那么实际上还是降低这个数值比较好,这样可以把尖峰的写操作削平成多次写操作。缺省数值是500,也就是 5 秒。设置方法如下:echo 200 >/proc/sys/vm/dirty_writeback_centisecs
  • /proc/sys/vm/dirty_expire_centisecs: 这个参数声明Linux内核写缓冲区里面的脏数据多“旧”了之后,pdflush 进程就开始考虑写到磁盘中去。单位是 1/100秒。对于特别重载的写操作来说,这个值适当缩小也是好的,但也不能缩小太多,因为缩小太多也会导致IO提高太快。缺省是 30000,也就是 30 秒的数据就算旧了,将会刷新磁盘。建议设置为 1500,也就是15秒算旧。设置方法如下:echo 1500 >/proc/sys/vm/dirty_expire_centisecs


4. JAVA File MMAP总结


  • MMAP对于文件读写效率最快,内存映射文件提供了Java有可能达到的最快文件IO操作
  • MMAP最大可以Map小于但是不包含2G大小(2GB - 1B)的内存
  • 读写内存映射文件是操作系统来负责的,因此,即使你的Java程序在写入内存后就挂掉了,只要操作系统工作正常,数据就会写入磁盘。如果电源故障或者主机瘫痪,有可能内存映射文件还没有写入磁盘,意味着可能会丢失一些关键数据。
  • 用于加载文件的内存在Java的堆内存之外,存在于共享内存中,允许两个不同进程访问文件。并且这块内存大小不受也不占用JVM内存(包括Xmx和XXMaxDirectMemory限定的内存)
  • 不要经常调用MappedByteBuffer.force()方法,这个方法强制操作系统将内存中的内容写入硬盘,所以如果你在每次写内存映射文件后都调用force()方法,你就不能真正从内存映射文件中获益,而是跟disk IO差不多。
  • 我们要想解除MMAP只能先把buffer置为null,然后祈祷GC赶紧起作用,实在等不及还可以用System.gc()催促一下GC赶快干活,不过后果是会引发FullGC。
  • 如果被请求的页面不在内存中,内存映射文件会导致缺页中断
  • 我们只能通过进程的Pss来统计文件映射内存的大小,top统计的内存占用不准确
  • 通过合理的pdflush参数调优,我们能进一步优化MMAP的性能


5. 参考


相关文章
|
6天前
|
运维 Java
Java版HIS系统 云HIS系统 云HIS源码 结构简洁、代码规范易阅读
云HIS系统分为两个大的系统,一个是基层卫生健康云综合管理系统,另一个是基层卫生健康云业务系统。基层卫生健康云综合管理系统由运营商、开发商和监管机构使用,用来进行运营管理、运维管理和综合监管。基层卫生健康云业务系统由基层医院使用,用来支撑医院各类业务运转。
30 5
|
1天前
|
存储 安全 Java
Java并发编程中的高效数据结构:ConcurrentHashMap解析
【4月更文挑战第25天】在多线程环境下,高效的数据访问和管理是至关重要的。Java提供了多种并发集合来处理这种情境,其中ConcurrentHashMap是最广泛使用的一个。本文将深入分析ConcurrentHashMap的内部工作原理、性能特点以及它如何在保证线程安全的同时提供高并发性,最后将展示其在实际开发中的应用示例。
|
1天前
|
搜索推荐 前端开发 Java
java医院绩效考核管理系统项目源码
系统需要和his系统进行对接,按照设定周期,从his系统获取医院科室和医生、护士、其他人员工作量,对没有录入信息化系统的工作量,绩效考核系统设有手工录入功能(可以批量导入),对获取的数据系统按照设定的公式进行汇算,且设置审核机制,可以退回修正,系统功能强大,完全模拟医院实际绩效核算过程,且每步核算都可以进行调整和参数设置,能适应医院多种绩效核算方式。
3 0
|
2天前
|
Java
Java输入输出流详细解析
Java输入输出流详细解析
Java输入输出流详细解析
|
2天前
|
设计模式 算法 Java
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
|
2天前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
|
2天前
|
设计模式 存储 JavaScript
[设计模式Java实现附plantuml源码~创建型] 多态工厂的实现——工厂方法模式
[设计模式Java实现附plantuml源码~创建型] 多态工厂的实现——工厂方法模式
|
2天前
|
设计模式 Java Go
[设计模式Java实现附plantuml源码~创建型] 集中式工厂的实现~简单工厂模式
[设计模式Java实现附plantuml源码~创建型] 集中式工厂的实现~简单工厂模式
|
2天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
28 1
|
2天前
|
存储 Java C++
Java集合篇之深度解析Queue,单端队列、双端队列、优先级队列、阻塞队列
Java集合篇之深度解析Queue,单端队列、双端队列、优先级队列、阻塞队列
16 0

推荐镜像

更多