浅析FileInputStream#read方法

2022-11-14 09:21:39 浏览数 (1)

本篇文章使用的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[].

0 人点赞