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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 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. 参考


相关文章
|
20天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
57 7
|
3天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
1天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
3天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
17 3
|
3天前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
22 2
|
12天前
|
存储 JavaScript 前端开发
基于 SpringBoot 和 Vue 开发校园点餐订餐外卖跑腿Java源码
一个非常实用的校园外卖系统,基于 SpringBoot 和 Vue 的开发。这一系统源于黑马的外卖案例项目 经过站长的进一步改进和优化,提供了更丰富的功能和更高的可用性。 这个项目的架构设计非常有趣。虽然它采用了SpringBoot和Vue的组合,但并不是一个完全分离的项目。 前端视图通过JS的方式引入了Vue和Element UI,既能利用Vue的快速开发优势,
72 13
|
20天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
23天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
23天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
22天前
|
人工智能 移动开发 安全
家政上门系统用户端、阿姨端源码,java家政管理平台源码
家政上门系统基于互联网技术,整合大数据分析、AI算法和现代通信技术,提供便捷高效的家政服务。涵盖保洁、月嫂、烹饪等多元化服务,支持多终端访问,具备智能匹配、在线支付、订单管理等功能,确保服务透明、安全,适用于家庭生活的各种需求场景,推动家政市场规范化发展。