[root@node1 ~]# sysctl -a | grep dirty
vm.dirty_background_ratio = 0
vm.dirty_background_bytes = 1048576
vm.dirty_ratio = 0
vm.dirty_bytes = 1048576
vm.dirty_writeback_centisecs = 5000
vm.dirty_expire_centisecs = 30000
解释一下,这几个配置的含义:
vm.dirty_background_ratio
:内存可以填充脏数据的百分比。脏数据大小达到指定的内存的百分比的时候,才会写入磁盘。比如内存大小为10G,配置该项值为90,意思是可以有10G*90%=9G的脏数据待在内存,超过9G才会由后台进程来清理(写入磁盘)。vm.dirty_ratio
:可以用脏数据填充的绝对最大系统内存量,当系统到达此点时,必须将所有脏数据提交到磁盘,同时所有新的I/O
块都会被阻塞,直到脏数据被写入磁盘。这通常是长I/O
卡顿的原因,但这也是保证内存中不会存在过量脏数据的保护机制。vm.dirty_background_bytes
和vm.dirty_bytes
是另一种指定这些参数的方法。如果设置_bytes版本
,则_ratio
版本将变为0,反之亦然。vm.dirty_expire_centisecs
: 指定脏数据能存活的时间。vm.dirty_writeback_centisecs
:指定多长时间清理脏数据的进程会唤醒一次,然后检查是否有缓存需要清理。[root@node1 testfileio]# vi /etc/sysctl.conf
# 后台方式,内存假设可用10个G,在程序使用IO的时候,一直到占用了9个G的时候,才会真正写入到磁盘(这时还会继续IO,只是会再起一个线程把数据写入到磁盘)---可能会丢数据
vm.dirty_background_ratio = 90
# 假设程序疯狂地向内核写数据,达到可用内存的90%,就不会继续写
vm.dirty_ratio = 90
# 任务线程时间的维度
# 50s一次写入磁盘
vm.dirty_writeback_centisecs = 5000
# 300s延时,也就是说虚拟机突然断电,这个时间应该不会写入磁盘,这个时间配置为了演示pagecache会丢数据
vm.dirty_expire_centisecs = 30000
[root@node1 ~]# sysctl -p
rm -rf *out*
/root/soft/jdk1.8.0_131/bin/javac OSFileIO.java
strace -ff -o out /root/soft/jdk1.8.0_131/bin/java OSFileIO $1
这个脚本的意思就是执行OSFileIO这个Java程序,并用strace追踪Java程序运行过程中与磁盘IO交互的过程,并记录到out文件中。
public static void main(String[] args) {
//whatIsByteBuffer();
switch (args[0]) {
case "0":
basicFileIO();
break;
case "1":
bufferFileIO();
break;
case "2":
randomAccessFileWrite();
break;
default:
break;
}
}
调用:./test.sh 0
表示运行java程序,并传入参数0,也就是执行
case "0"
分支,等等
基本File IO的写操作
/**
* 最基本的File写操作
*/
public static void basicFileIO(){
File file = new File(path);
try {
FileOutputStream out = new FileOutputStream(file);
//不停写入数据,配合给虚拟机断电,观察pagecache
while (true) {
out.write(data);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
执行 ./test.sh 0
,观察out.txt
文件大小变化(程序不停的向out.txt
文件写数据):
[root@node1 testfileio]# ./test.sh 0
再开启一个连接这台虚拟机的标签页,用命令ll -h && pcstat out.txt
观察被写入的文件out.txt
的大小变化,以及它在OS中的缓存情况。由于basicFileIO
方法写的是死循环不停的写入,可以不停的执行命令观察。
下面截取三个时间点的运行情况:
从图中暂时可以得出一个结论:用基本File IO的方式,文件写入的速度不快。(实际操作观察时发现,每次
ll -h
查看文件大小增长不快。)
此时直接给虚拟机断电,由于前面我们配置的是脏数据在内存中占到90%的时候才写入磁盘,而此时才写到10几M左右,数据仍在内存中,所以大胆猜测一下:断电后写入到out.txt文件中的数据将丢失!!!
启动虚拟机,验证一下,依然是执行ll -h && pcstat out.txt
out.txt appears to be 0 bytes in length
数据全部丢失了!
因此可以得出结论:PageCache是优化IO性能的东东,但是也会丢失数据的。
演练一下,看看Buffer IO是否比上面基本的File IO速度快点。
public static void bufferFileIO() {
File file = new File(path);
try {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
while (true) {
out.write(data);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
执行./test.sh 1
,运行bufferFileIO
方法,用ll -h && pcstat out.txt
观察文件大小变化。
文件增长太快,来不及截图了。。。
进行断电操作,同样能验证pagecache会丢失数据的特点。
此时又能得出一个结论了:Java使用Buffered IO(比如Buffered
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file))
)操作比基本的(比如FileOutputStream out = new FileOutputStream(file)
)文件操作速度快。原因:
JVM中使用Buffer时会开辟一个8KB大小的字节数组,程序是每次写10个字节,这10个字节并没有交给内核,而是放在了JVM开辟的字节数组,8KB满了以后,才会调用一次内核的
syscall write
。而普通的文件IO操作,是写满10字节后直接调用内核的
syscall write
。也就是说在用户态与内核态的切换上,Buffer IO操作明显比普通的文件IO操作少,所以它快一些。
由前面的结论得知Java IO的Buffer操作性能好,Java NIO很多新的功能也是基于buffer的,先来看一下
ByteBuffer
这个东东。
看代码
public static void whatIsByteBuffer(){
//在JVM堆上分配
//ByteBuffer buffer = ByteBuffer.allocate(1024);
//在JVM堆外分配(直接内存)
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
System.out.println("================ ByteBuffer ==============");
System.out.println("position:" + buffer.position());
System.out.println("limit:" + buffer.limit());
System.out.println("capacity:" + buffer.capacity());
System.out.println("bytebuffer mark:" + buffer);
//写
buffer.put("666".getBytes());
System.out.println("=============== buffer put 666 ============");
System.out.println("bytebuffer mark:" + buffer);
//读写交替
buffer.flip();
System.out.println("=============== buffer flip ============");
System.out.println("bytebuffer mark:" + buffer);
//读
byte b = buffer.get();
System.out.println("=============== buffer get ============");
System.out.println("buffer get :" + b);
System.out.println("bytebuffer mark:" + buffer);
buffer.compact();
System.out.println("=============== buffer compact ============");
System.out.println("bytebuffer mark:" + buffer);
buffer.clear();
System.out.println("=============== buffer clear ============");
System.out.println("bytebuffer mark:" + buffer);
}
ByteBuffer可以理解为一个字节数组。
ByteBuffer的两种内存分配方式
ByteBuffer.allocate(1024)
和ByteBuffer.allocateDirect(1024)
不影响执行api结果。
position
偏移指针limit
大小限制capacity
总容量大小bytebuffer初始状态:
buffer.put("666".getBytes())
后,position指针移动三个字节:
所以put 3字节后运行结果:java.nio.DirectByteBuffer[pos=3 lim=1024 cap=1024]
如果想要读取bytebuffer,必须先flip一下,将position指针移动到0的位置,limit指针移动到之前写入的位置:
所以flip运行结果 java.nio.DirectByteBuffer[pos=0 lim=3 cap=1024]
filp以后就可以get了,每次get不传参数的话,get一个字节:
所以get后运行结果:java.nio.DirectByteBuffer[pos=1 lim=3 cap=1024]
由于前面flip将limit指针移动到最近一次写入的位置,如果想要继续使用剩余的bytebuffer空间进行写入,需要调用compact,将前面get到的挤压
掉,position来到剩余空间的开始位置,limit回到最大的位置:
运行结果:java.nio.DirectByteBuffer[pos=2 lim=1024 cap=1024]
调用清除clear
就好理解了。
RandomAccessFile可以随机访问文件的内容,可通过seek来定位内容位置,并可以直接write数据到文件。
FileChannel 文件通道,终于入门Java NIO了!
MappedByteBuffer 只有文件通道才有mmap映射,socket通道没有。mmap是堆外的,和文件映射的东西。
来看一段代码
/**
* 文件NIO
*/
public static void randomAccessFileWrite() {
try {
RandomAccessFile raf = new RandomAccessFile(path, "rw");
raf.write("hello world\n".getBytes());
raf.write("hello china\n".getBytes());
System.out.println("-------------- RandomAccessFile written ---------------");
System.in.read();//阻塞住,按回车键继续执行
raf.seek(4);
raf.write("xxoo".getBytes());
System.out.println("-------------- RandomAccessFile seek ---------------");
System.in.read();
//Java NIO来了!!!
FileChannel channel = raf.getChannel();
//mmap jvm堆外的 和文件映射的
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
map.put("@@@".getBytes());//不是系统调用,但是数据会到达内核的pagecache
System.out.println("------------- MappedByteBuffer map put ------------");
System.in.read();
raf.seek(0);
ByteBuffer buffer = ByteBuffer.allocate(8192);
//ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
int read = channel.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)));
}
} catch (Exception e) {
e.printStackTrace();
}
}
程序在等待着输入,这时看一下文件内容:
那么此时out.txt
的内容在磁盘上吗?
不在,在pagecache,因为还没有做刷入的操作。
按回车键,继续往下执行:
说明seek完了,再来看一下out.txt
:
执行完
raf.seek(4);
raf.write("xxoo".getBytes());
这两句代码后,发现文件内容从seek的位置重新写入了。
这就是RandomAccessFile
的随机读写能力。
此时程序还在继续运行(用System.in.read()
阻塞住了),用jps
查看java进程,并使用lsof -p
查看进程产生的一些文件描述:
由图中可以看出,out.txt并没有mem
的描述,说明 还没有建立起内存与文件的映射。
回到程序运行界面,按下回车,继续运行下面这段代码:
//Java NIO来了!!!
FileChannel channel = raf.getChannel();
//mmap jvm堆外的 和文件映射的
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
map.put("@@@".getBytes());//不是系统调用,但是数据会到达内核的pagecache
System.out.println("------------- MappedByteBuffer map put ------------");
运行完了,再来lsof -p
发现这次有文件内存映射了!!!
这个时候看一下out.txt的内容:
“@@@”字符写入了(map.put("@@@".getBytes())
),并且文件大小涨到了4096(channel.map(FileChannel.MapMode.READ_WRITE, 0, 4096)
)
说一下map.put
它不是系统调用(
syscall
),但是数据会到达内核的pagecache。之前我们是需要
out.write()
这样的系统调用,才能让程序的data进入内核的pagecache,也就是说之前必须有用户态内核态切换。但是mmap的内存映射,依然是内核的pagecache体系所约束的!!!也就是说会丢数据。
C语言写的jni扩展库,可使用linux内核的Direct IO—直接IO。直接IO是忽略linux的pagecache的。它是交给了程序自己开辟一个字节数组当作pagecache,但是仍需要动用代码逻辑来维护一致性/dirty等一系列复杂问题。