MMU虚拟内存映射
应用程序本身是不能直接访问物理内存的,主要是为了安全,在每个app看来仿佛自己占有了整个物理内存空间,实际上是一个线性地址到物理地址映射的过程,一个程序所占用的物理地址是不连续的(因为随用随分配,线性地址映射到的物理地址不一定连续),而映射的过程需要MMU(内存管理单元)控制,而依赖的最小单位是页,默认一页是4kb,因为随用随申请的的特点,在运行过程中会出现缺页,缺页会引发缺页中断,会从磁盘中加载想要的页到内存,这之间涉及页面置换算法,一般就是LRU,也就是将需要的加载进来,最久未使用的淘汰掉.
page cache
Page cache是通过将磁盘中的数据缓存到内存中,从而减少磁盘I/O操作,从而提高性能。此外,还要确保在page cache中的数据更改时能够被同步到磁盘上,后者被称为page回写。比如一个进程的运行需要将代码段数据段等加载到内存,这些数据会一页一页的缓存到page cache,如果再起一个同样的进程不需要再加载一遍程序到内存,可以映射到page cache缓存的同一个代码段,但如果操作各自的数据段会类似copyonwrite.如果操作同一个文件,这个文件也会被缓存到page cache,只不过两个程序时打开两个不同的文件描述符,分别维护对这个文件在page cache的偏移量.
对磁盘的数据进行缓存从而提高性能主要是基于两个因素:
- 第一磁盘访问的速度比内存慢好几个数量级(毫秒和纳秒的差距).
- 第二是被访问过的数据,有很大概率会被再次访问.
page cache占比
//查看系统控制项关于脏页的
sysctl -a | grep dirty
脏页
缓存中与磁盘中数据不一致的页都是脏页,脏页是要落磁盘的.比如新写一个文件的时候先写到page cache,这时没有落磁盘之前就是脏页,如果落磁盘了又对其修改了也是脏页.
[root@vm106 ~]# sysctl -a | grep dirty
#脏页达到可用内存的10%就会向磁盘中写,这个参数是后台的,也就是
#内核会有一个线程执行写的过程,比如4个G内存,当到400多M的时候就会写进磁盘
#也就是缓存区占总内存大小
vm.dirty_background_ratio = 10
vm.dirty_background_bytes = 0
#与vm.dirty_background_ratio一致,只不过是前台的,需要阻塞写
vm.dirty_ratio = 20
vm.dirty_bytes = 0
#时间控制,0.5秒向磁盘写一次
vm.dirty_writeback_centisecs = 500
#脏页生命周期
vm.dirty_expire_centisecs = 3000
可在/etc/sysctl.conf中修改.
修改为
vm.dirty_background_ratio = 90
vm.dirty_ratio = 90
vm.dirty_writeback_centisecs = 50000
vm.dirty_expire_centisecs = 30000
基本文件写
//最基本的file写
static byte[] data = "123456789\n".getBytes();
static String path = "/root/testfileio/out.txt";
public static void testBasicFileIO() throws Exception {
File file = new File(path);
FileOutputStream out = new FileOutputStream(file);
while(true){
Thread.sleep(10);
out.write(data);
}
}
直接使用java文件输出流写,而且并没有调用flush,速度是很慢的, 写一段时间直接非正常关闭虚拟机发现文件内容为0,也就是全部丢失.因为这段时间里写的内容都被page cache缓存了(上面设置了阈值90%),此时关闭虚拟机page cache的数据没来得及写进磁盘,所以全部丢失,如果写了超过了page cache的阈值,那么数据会被写进磁盘,因为到达阈值触发了写磁盘操作.
缓存写
//测试buffer文件IO
static byte[] data = "123456789\n".getBytes();
static String path = "/root/testfileio/out.txt";
public static void testBufferedFileIO() throws Exception {
File file = new File(path);
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
while(true){
Thread.sleep(10);
out.write(data);
}
}
使用BufferedOutputStream文件写,速度明显快于直接文件流写,在写的过程中,前面直接文件流写的page cache缓存的页会越来越小,逐渐被写入磁盘,而缓存写会占用越来越大的page cache,当达到阈值会写进磁盘.
缓存流写比直接文件流写快的原因
直接文件流写是每调用一次write则将"123456789\n"这10个字节调用一次系统调用write写到page cache,而缓存流写是在JVM里开辟了一个8k 的内存缓冲区,当写满8k时再调用系统调用写到page cache,因为缓存流调用的系统调用次数少,用户态内核态切换次数少,所以要比直接写快的多.
注:调用flush会强制把page cache脏页写到磁盘.
NIO
@Test
public void whatByteBuffer(){
// ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
System.out.println("postition: " + buffer.position());
System.out.println("limit: " + buffer.limit());
System.out.println("capacity: " + buffer.capacity());
System.out.println("mark: " + buffer);
buffer.put("123".getBytes());
System.out.println("-------------put:123......");
System.out.println("mark: " + buffer);
buffer.flip(); //读写交替
System.out.println("-------------flip......");
System.out.println("mark: " + buffer);
buffer.get();
System.out.println("-------------get......");
System.out.println("mark: " + buffer);
buffer.compact();
System.out.println("-------------compact......");
System.out.println("mark: " + buffer);
buffer.clear();
System.out.println("-------------clear......");
System.out.println("mark: " + buffer);
}
-----------------输出--------------------
postition: 0
limit: 1024
capacity: 1024
mark: java.nio.DirectByteBuffer[pos=0 lim=1024 cap=1024]
-------------put:123......
mark: java.nio.DirectByteBuffer[pos=3 lim=1024 cap=1024]
-------------flip......
mark: java.nio.DirectByteBuffer[pos=0 lim=3 cap=1024]
-------------get......
mark: java.nio.DirectByteBuffer[pos=1 lim=3 cap=1024]
-------------compact......
mark: java.nio.DirectByteBuffer[pos=2 lim=1024 cap=1024]
-------------clear......
mark: java.nio.DirectByteBuffer[pos=0 lim=1024 cap=1024]
文件NiO可通过ByteBuffer.allocate(1024);在堆上分配一个缓冲区,而ByteBuffer.allocateDirect(1024);可在堆外(java进程的堆)分配一个缓冲区,这个缓冲区有三个指针,分别是postition,limit,capacity.
pos代表当前位置,limit主要用来记录位置,因为pos会移动,cap代表最大容量.起始时,pos指向起始位置,cap和limit都指向最大位置,当put(“123”)时,会向buffer中加入三个字节,所以pos向右移动了三个在3的位置.
当调用buffer.flip()时代表读写交替,从写变为读,pos会移动到起始位置,因为要记录写到哪了,所以limit移动到pos的位置.
当调用buffer.get()时,从buffer中读出一个字节,所以pos会向右移动一个字节.
当调用buffer.compact()时会挤压一下,将已读的清除,将未读的向前挪动,然后pos回到写的位置,limit移动到最后,(当调用带索引的get(index)时,compact不会清除已读,会回到写的位置,limit移动到最后)
buffer.clear()将buffer情况,回到起始位置.
文件NIO
public static void testRandomAccessFileWrite() throws Exception {
RandomAccessFile raf = new RandomAccessFile(path, "rw");
raf.write("hello mashibing\n".getBytes());
raf.write("hello seanzhou\n".getBytes());
System.out.println("write------------");
System.in.read();
raf.seek(4);
raf.write("ooxx".getBytes());
System.out.println("seek---------");
System.in.read();
FileChannel rafchannel = raf.getChannel();
//mmap 堆外 和文件映射的 byte not objtect
MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
map.put("@@@".getBytes()); //不是系统调用 但是数据会到达 内核的pagecache
//曾经我们是需要out.write() 这样的系统调用,才能让程序的data 进入内核的pagecache
//曾经必须有用户态内核态切换
//mmap的内存映射,依然是内核的pagecache体系所约束的!!!
//换言之,丢数据
//你可以去github上找一些 其他C程序员写的jni扩展库,使用linux内核的Direct IO
//直接IO是忽略linux的pagecache
//是把pagecache 交给了程序自己开辟一个字节数组当作pagecache,动用代码逻辑来维护一致性/dirty。。。一系列复杂问题
System.out.println("map--put--------");
System.in.read();
// map.force(); // flush
raf.seek(0);
ByteBuffer buffer = ByteBuffer.allocate(8192);
// ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
int read = rafchannel.read(buffer); //buffer.put()
System.out.println(buffer);
buffer.flip();
System.out.println(buffer);
for (int i = 0; i < buffer.limit(); i++) {
Thread.sleep(200);
System.out.print(((char)buffer.get(i)));
}
}
RandomAccessFile随机文件读写,与一般的文件读写不同点在于能够随时调整偏移,也就是seek().raf.getChannel()能拿到一个可读写的channel,而这个channel能够通过map()方法获取一个mmap的映射,能够直接进入page cache,且不经过系统调用.
三个位置
- on heap:堆上分配,ByteBuffer.allocate(1024),在JVM的堆上分配一个缓冲区,写入时需要先复制到堆外内存,再从堆外复制到page cache,最后写到磁盘.
- off heap:堆外分配,ByteBuffer.allocateDirect(1024);或者使用unsafe.allocateMemory();(unsafe的需要调用freeMemory()回收,ByteBuffer的GC好像能够回收)不在JVM的堆里,而是在Java进程的堆里(jvm的堆只是java堆的一块分配区域,根据参数-Xms和-Xmx决定,区别就是堆内可以直接存储对象,受GC管理,堆外只能使用字节数组,不受GC管理),它相对于jvm堆,少了一步复制到堆外的过程,直接复制到page cache再写到磁盘.
- mmap:raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 4096);使用RandomAccessFile获取channel可获取到一个mmap的映射缓冲区,直接对接page cache,不产生系统调用.
性能:mmap > off heap > on heap
注:三种方式都没有摆脱linux的page cache体系,如果想不被约束,需要用C.