3.2.2. 对MappedByteBuffer进行读写
对于MappedByteBuffer的读写和对于ByteBuffer的读写是一样的,可以参考我的另一片文章:https://blog.csdn.net/zhxdick/article/details/5
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内核线程将系统中的脏页写到磁盘上,在下面几种情况下:
- 定时方式: 定时机制定时唤醒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()。
- 内存不足的时候: 这时并不将所有的dirty页写到磁盘,而是每次写大概1024个页面,直到空闲页面满足需求为止。
- 写操作时发现脏页超过一定比例: 当脏页占系统内存的比例超过/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内核中是没有的。
- 用户调用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的性能