最近看到网上有些文章在讨论JAVA中普通文件IO读/写的时候经过了几次数据拷贝,如果从系统调用开始分析,以读取文件为例,数据的读取过程如下(以缓存I/O为例):

- 应用程序调用read函数发起系统调用,此时由用户空间切换到内核空间;
- 内核通过DMA从磁盘拷贝数据到内核缓冲区;
- 将内核缓冲区的数据拷贝到用户空间的缓冲区,回到用户空间;
整个读取过程发生了两次数据拷贝,一次是DMA将磁盘上的文件数据拷贝到内核缓冲区,一次是将内核缓冲区的数据拷贝到用户缓冲区。
在JAVA中,JVM划分了堆内存,平时创建的对象基本都在堆中,不过也可以通过NIO包下的 ByteBuffer 申请堆外内存 DirectByteBuffer :
ByteBuffer.allocateDirect(size);

无论是普通IO或者是NIO,在进行文件读写的时候一般都会创建一个buffer作为数据的缓冲区,读写相关方法底层是通过调用native函数(JNI调用)来实现的,在进行读写时将buffer传递给JNI。
JNI一般使用C/C++代码实现,JNI底层调用C函数库时,要求buffer所在内存地址上的内容不能失效,但是JVM在进行垃圾回收的时候有可能对对象进行移动,导致地址发生变化,所以通过NIO进行文件读取的时候,从源码中可以明显看到对buffer的对象类型进行了判断,如果buffer是 DirectByteBuffer 类型,使用的是堆外内存,直接使用即可,反之则认为使用的是堆内内存,此时需要先申请一块堆外内存作为堆外内存buffer,然后进行系统调用,进行数据读取,读取完毕后将堆外内存buffer的内容再拷回JVM堆内内存buffer中,这里一般是没有疑问的。
比较有疑问的点是在普通IO中,读写文件传入的是字节数组 byte[] ,一种说法是数组一般分配的是连续的内存空间,即使内存地址发生了变化,根据数组的首地址依旧可以找到整个数组的内存,所以使用普通IO进行文件读写的时候,不需要重新分配堆外内存,直接使用堆内的字节数组即可,为了探究普通IO到底有没有重新申请堆外内存,接下来我们去看下源码。
普通IO
首先来看一下使用普通IO进行文件读取的例子,创建一个文件输入流和字节数组,通过输入流读取文件到字节数组中, 这里的字节数组占用的是JVM的堆内内存 :
// 创建输入流
try (InputStream is = new FileInputStream("/document/123.txt")) {
// 创建字节数组(堆内内存)
byte[] bytes = new byte[1024];
int len = 0;
// 通过read方法读取数据到bytes数组
while ((len = is.read(bytes)) != -1){
String content = new String(bytes, 0, len);
System.out.print(content);
}
is.read(bytes);
} catch (Exception e) {
e.printStackTrace();
}
由于输入流使用的 FileInputStream ,所以读取文件会进入到 FileInputStream 中的 read 方法,可以看到里面又调用了 readBytes 方法, readBytes 是一个native方法,里面传入了三个参数,分别为存放数据的字节数组、读取文件的起始位置和读取数据的长度:
public class FileInputStream extends InputStream {

本文探讨JAVA中普通IO和NIO在读写文件时的数据拷贝次数。在普通IO中,数据经历了从磁盘到内核缓冲区,再到用户空间的堆外内存,最后拷贝到堆内内存的字节数组,共三次拷贝。而在NIO中,根据Buffer类型,可能直接使用堆外内存,减少了拷贝次数。通过对源码的分析,揭示了数据读取的详细流程。
最低0.47元/天 解锁文章
479

被折叠的 条评论
为什么被折叠?



