本篇文章使用的JVM源码版本是jdk8-b116
在FileInputStream.java文件中,有4个和读操作相关的方法,其中有2个方法属于native方法.
代码语言:javascript复制
public native int read() throws IOException;
private native int readBytes(byte b[], int off, int len) throws IOException;
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
首先看下native read()方法.
它的实现在jdk/src/share/native/java/io/FileInputStream.c
它内部调用readSingle方法.
readSingle实现在jdk/src/share/native/java/io/io_util.c
readSingle内部调用了IO_Read方法, 关注下第三个参数值1
在C程序里, 如果读取到文件末尾,那么read系统调用会返回0,从上面的JVM源码可以看到, Java做了一个中转, 如果读取到文件末尾, 返回的是-1, 并不是0, 这也是为什么在我们的程序中要根据-1来判断是否读取到文件末尾的原因, 而C程序是根据返回值等于0判断读取到文件末尾. 我们可以通过 man 2 read 查看手册瞧一瞧
在jdk/src/solaris/native/java/io/io_util_md.h头文件中定义了IO_Read即handleRead
继续查看handleRead方法.
handleRead实现在jdk/src/solaris/native/java/io/io_util_md.c
handleRead内部调用了read这个系统调用. 入参len值就是从上面传递下来的1.
也就是说, 在我们的Java程序中调用FileInputStream的read()方法, JVM会向操作系统读取1个字节数据.
而另一个native readBytes(byte b[], int off, int len)入参可以指定一个字节数组,指定一次性读取多少字节. 它的内部和上面说的native read()一样, 区别在调用系统调用read方法时传的len参数值是程序员设置的len值.
接下来做个实验
代码语言:javascript复制
package com.infuq;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class ReadFile {
public static void main(String[] args) throws IOException {
final String path = "/home/v-infuq/tmp/1.txt";
File file = new File(path);
FileInputStream fileInputStream = new FileInputStream(file);
System.out.println("begin read 1.txt");
int d = fileInputStream.read();
System.out.println("" d);
d = fileInputStream.read();
System.out.println("" d);
d = fileInputStream.read();
System.out.println("" d);
d = fileInputStream.read();
System.out.println("" d);
d = fileInputStream.read();
System.out.println("" d);
fileInputStream.close();
}
}
在1.txt文件中写入的是infuq
通过strace命令可以追踪程序的系统调用.
strace命令运行完成之后, 会生成很多out.*文件, 由于在ReadFile.java程序中输出了'begin read 1.txt', 使用grep命令搜索这个关键字在哪个out文件中, 它在out.2227文件中, 也就是说main线程打印的系统调用信息都在out.2227文件里. 如果大家对JVM启动稍微熟悉点的话, 可以直接断定第二个out文件一定就是main线程对应的文件. 因为main线程是JVM启动过程中的第二个线程.
通过查看out.2227文件内容,如下
由于程序调用了5次read方法, 因此在out.2227文件中打印的系统调用read也是5次, 且每次只读取1个字节.
改写一下程序
代码语言:javascript复制
package com.infuq;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class ReadFile {
public static void main(String[] args) throws IOException {
final String path = "/home/v-infuq/tmp/1.txt";
File file = new File(path);
FileInputStream fileInputStream = new FileInputStream(file);
System.out.println("begin read 1.txt");
byte[] buf = new byte[8];
int length = 0;
while((length = fileInputStream.read(buf)) != -1){
System.out.print(new String(buf,0,length));
}
fileInputStream.close();
}
}
如上, 执行相同的操作, 查看out.2676文件
它只调用了一次read方法就把数据(infuq)读取完成了.
总结
直接调用read()方法每次只能读取1个字节. 可以通过read(byte[])或read(byte[],int,int)一次读取多个字节. 这样的话, 读取磁盘上相同的文件, 后者比前者减少了系统调用次数, 也就提高了读取效率. 这也是为什么BufferedInputStream比FileInputStream读取效率高的原因, 因为在BufferedInputStream内部有一个默认8192大小的byte[].