一、传统IO模式下的文件读取
传统的文件IO操作都是调用OS提供的底层标准IO操作读取函数read()、write();然后调用此函数的进程(即java进程)由java用户态切换至内核态。然后OS内核代码负责将相应的文件读取到内核IO缓存之中,然后再把数据从内核IO缓存之中拷贝到进程相应的私有地址空间中。则完成一次IO操作;
Q&A:
Q:为什么要搞一个内核IO缓存,将原本拷贝一次数据的操作整两次?
A:因为为了减少磁盘的IO操作,提升性能;因为我们的程序具有局部性,即所谓的局部性原理,在这里是空间局部性;即我们访问文件中的一段数据,接下来可能还会访问接下去的一段数据,而磁盘的IO操作相对于内存慢了好几个数量级,所以OS根据局部性原理会在read()时,会预读更多的文件数据存放在内核IO缓存中,当访问的文件数据在内核IO缓冲区之中时直接将数据拷贝到进程私有内存地址之中(也有可能经过了native堆中转,因为这些函数都是声明为native本地平台相关)。避免低效率的磁盘IO读取。
Q:既然有内核IO缓存,那java为什么还提供BufferedInputStream对象?
A:因为从内核IO缓存中拷贝到进程私有空间数据系统调用,而系统调用相对来数代价是比较高的,需要从java用户态和内核态的上下文切换。
二,java NIO读取模式
- java内存映射文件读取模式
简介:将进程的用户私有空间的一部分区域与文件对象建立映射关系。并不需要将文件拷贝到内核IO缓存之中,类似于直接从内存中读取数据,这样速度当然快。
java之中针对于内存映射提供了三种模式:只读(readonly)、读写(read_write)、专有(private);
-
readonly:异常:针对只读模式,如果尝试写操作,则ReadonlyBufferException
-
read_write:可以进行读写操作,表明在通过内存文件映射的方式写或者修改能直接反应到文件对象中去; 如果其他进程共享了文件对象,那个能直接看到变化;不像标准IO模式,每个进程都有自己的内核缓冲;类似于java进程,必须要flash()或者close()操作才能将修改更正到磁盘文件对象中去;
-
write:采用OS“写时拷贝”原则,在没有进程写操作的时候,多个进程之间都是共享文件的同一快物理内存(即各个进程的虚拟地址指向同一片物理地址);一旦某个进程进行写操作,则会将受影响的文件数据单独拷贝一份到进程的私有缓冲区中去,而不会反映到物理文件中去;
备注:
-
java中对于进程单次文件IO限制Integer.MAX_VALUE,即2G左右,但是可以通过分次映射文件不同部分来达到操作整个文件的目的。
image.png
- java内存映射文件数据JVM直接缓冲区, 还可以通过 ByteBuffer.allocateDirect() ,即DirectMemory的方式来创建直接缓冲区。他们相比基础的 IO操作来说就是少了中间缓冲区的数据拷贝开销。同时他们属于JVM堆外内存,不受JVM堆内存大小的限制。
package com.seriousty.practice.memory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* Created with IntelliJ IDEA
* Created By seriousty
* Date: 2018/8/24
* Time: 15:05
* BOLG: [https://github.com/seriousty](https://github.com/seriousty)
* Description:
*/
public class IOTest {
private static int MAX_SIZE = 1024;
/**
*NIO操作:
*/
public static void main(String[] args) {
File file=new File("[h://iofile.pdf](file:///h://iofile.pdf)");
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
FileChannel channel = fis.getChannel();
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());//time consuming:39
byte[] bytes=new byte[1024];
int length = (int) file.length();
long start = System.currentTimeMillis();
for (int i = 0; i <length ; i+=1024) {
if(length-i>MAX_SIZE){
map.get(bytes);
}else{
map.get(new byte[length-i]);
}
}
long end = System.currentTimeMillis();
System.out.println("time consuming:"+(end-start));//time consuming:41
} catch (Exception e) {
e.printStackTrace();
}
}
}
普通IO模式:
public static void main(String[] args) {
File file = new File("[h://iofile.pdf](file:///h://iofile.pdf)");
try {
FileInputStream fis = new FileInputStream(file);
FileChannel channel = fis.getChannel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
//ByteBuffer allocate = ByteBuffer.allocateDirect((int) channel.size());
long start = System.currentTimeMillis();
while (channel.read(allocate) != -1) {
allocate.flip();
allocate.clear();
}
long end = System.currentTimeMillis();
System.out.println("time consuming:"+(end-start));//time consuming:1014
} catch (Exception e) {
e.printStackTrace();
}
}
2,直接内存(Directed Memory)
-
直接内存大小默认等同于JVM堆大小 -Xmx:JVM堆大小;
-
但是直接内存大小不受JVM堆的限制 由JVM参数 -XX:MaxDirectMemorySize 单独配置
给直接内存设置最大内存大小

给直接内存分配内存

结论:
执行了多次Full GC,没有执行GC(不受新生代的影响),只有当回收老年代的时候再回顺便回收直接内存?why,因为直接内存是通过DirectByteBuffer对象来引用的,所以当DirectByteBuffer对象由新生代进入老年代后,在老年代触发了Full GC.
总结:
NIO中的DirectMemory和内存映射文件都是直接内存缓冲,但是DirectMemory能通过JVM -XX:+Xmx和-XX:MaxDirectMemorySize参数来控制,内存映射文件没有JVM参数可以控制;
两者内存分配位置:
DirectMemory:在java进程中的native堆中分配,不受young gc控制,避免了在java堆和native堆中copy,提高文件传输的性能
放java向外进行数据传输时,需要先将数据从java堆拷贝到native堆之中。
内存映射文件:没有经过native堆,由java进程私有内存空间一部分于文件对象建立关联的映射关系。所以也不会受young gc影响。
网友评论