本文转载自JDK源码阅读-FileInputStream

导语

FileIntputStream用于打开一个文件并获取输入流。

打开文件

我们来看看FileIntputStream打开文件时,做了什么操作:

  1. public FileInputStream(File file) throws FileNotFoundException {
  2. String name = (file != null ? file.getPath() : null);
  3. SecurityManager security = System.getSecurityManager();
  4. if (security != null) {
  5. security.checkRead(name);
  6. }
  7. if (name == null) {
  8. throw new NullPointerException();
  9. }
  10. if (file.isInvalid()) {
  11. throw new FileNotFoundException("Invalid file path");
  12. }
  13. fd = new FileDescriptor();
  14. fd.attach(this);
  15. path = name;
  16. open(name);
  17. }
  18. private void open(String name) throws FileNotFoundException {
  19. open0(name);
  20. }
  21. private native void open0(String name) throws FileNotFoundException;

FileIntputStream的构造函数,在Java层面做的事情不多:

  1. 检查是否有读取文件的权限
  2. 判断文件路径是否合法
  3. 新建FileDescriptor实例
  4. 调用open0本地方法

FileDescriptor类对应操作系统的文件描述符,具体可以参考JDK源码阅读-FileDescriptor这篇文章。

  1. // jdk/src/share/native/java/io/FileInputStream.c
  2. JNIEXPORT void JNICALL
  3. Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
  4. // 使用O_RDONLY只读模式打开文件
  5. fileOpen(env, this, path, fis_fd, O_RDONLY);
  6. }
  7. // jdk/src/solaris/native/java/io/io_util_md.c
  8. void
  9. fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
  10. {
  11. WITH_PLATFORM_STRING(env, path, ps) {
  12. FD fd;
  13. #if defined(__linux__) || defined(_ALLBSD_SOURCE)
  14. // 如果是Linux或BSD,去掉path结尾的/,因为这些内核不需要
  15. char *p = (char *)ps + strlen(ps) - 1;
  16. while ((p > ps) && (*p == '/'))
  17. *p-- = '\0';
  18. #endif
  19. fd = JVM_Open(ps, flags, 0666); // 打开文件拿到文件描述符
  20. if (fd >= 0) {
  21. SET_FD(this, fd, fid); // 非负整数认为是正确的文件描述符,设置到fd字段
  22. } else {
  23. throwFileNotFoundException(env, path); // 负数认为是不正确文件描述符,抛出FileNotFoundException异常
  24. }
  25. } END_PLATFORM_STRING(env, ps);
  26. }

FileOutputStream#open的JNI代码逻辑也比较简单:

  1. 如果是Linux或BSD,去掉path结尾的/,因为这些内核不需要
  2. 调用JVM_Open函数打开文件,得到文件描述符
  3. 调用SET_FD设置文件描述符到FileDescriptor#fd

SET_FD用于设置文件描述符到FileDescriptor#fd,具体可以参考JDK源码阅读-FileDescriptor这篇文章。

JVM_Open根据其命名可以看得出来是JVM提供的函数,可以看出JDK的实现是分为多层的:Java-JNI-JDK,需要和操作系统交互的代码在JNI层面,一些每个操作系统都需要提供的真正底层的方法JVM来提供。具体的这个分层设计以后如果能有机会看JVM实现应该能有更深的理解。

JVM_Open的实现可以在Hotspot虚拟机的代码中找到:

  1. // hotspot/src/share/vm/prims/jvm.cpp
  2. JVM_LEAF(jint, JVM_Open(const char *fname, jint flags, jint mode))
  3. JVMWrapper2("JVM_Open (%s)", fname);
  4. //%note jvm_r6
  5. int result = os::open(fname, flags, mode); // 调用os::open打开文件
  6. if (result >= 0) {
  7. return result;
  8. } else {
  9. switch(errno) {
  10. case EEXIST:
  11. return JVM_EEXIST;
  12. default:
  13. return -1;
  14. }
  15. }
  16. JVM_END
  17. // hotspot/src/os/linux/vm/os_linux.cpp
  18. int os::open(const char *path, int oflag, int mode) {
  19. // 如果path长度大于MAX_PATH,抛出异常
  20. if (strlen(path) > MAX_PATH - 1) {
  21. errno = ENAMETOOLONG;
  22. return -1;
  23. }
  24. int fd;
  25. // O_DELETE是JVM自定义的一个flag,要在传递给操作系统前去掉
  26. int o_delete = (oflag & O_DELETE);
  27. oflag = oflag & ~O_DELETE;
  28. // 调用open64打开文件
  29. fd = ::open64(path, oflag, mode);
  30. if (fd == -1) return -1;
  31. // 问打开成功也可能是目录,这里还需要判断是否打开的是普通文件
  32. {
  33. struct stat64 buf64;
  34. int ret = ::fstat64(fd, &buf64);
  35. int st_mode = buf64.st_mode;
  36. if (ret != -1) {
  37. if ((st_mode & S_IFMT) == S_IFDIR) {
  38. errno = EISDIR;
  39. ::close(fd);
  40. return -1;
  41. }
  42. } else {
  43. ::close(fd);
  44. return -1;
  45. }
  46. }
  47. #ifdef FD_CLOEXEC
  48. // 设置文件描述符标志FD_CLOEXEC
  49. // 这样在fork和exec时,子进程就不会收到父进程打开的文件描述符的影响
  50. // 具体参考[FD_CLOEXEC用法及原因_转](https://www.cnblogs.com/embedded-linux/p/6753617.html)
  51. {
  52. int flags = ::fcntl(fd, F_GETFD);
  53. if (flags != -1)
  54. ::fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
  55. }
  56. #endif
  57. if (o_delete != 0) {
  58. ::unlink(path);
  59. }
  60. return fd;
  61. }

可以看到JVM最后使用open64这个方法打开文件,网上对于open64这个资料还是很少的,我找到的是man page for open64 (all section 2) - Unix & Linux Commands,从中可以看出,open64是为了在32位环境打开大文件的系统调用,但是不是标准的一部分。和open+O_LARGEFILE效果是一样的。参考:c - Wrapper for open() and open64() and see that system calls by vi uses open64() - Stack Overflow

这样完整的打开文件流程就分析完了,去掉各种函数调用,本质上只做了两件事:

  1. 调用open系统调用打开文件
  2. 保存得到的文件描述符到FileDescriptor#fd

读取文件

  1. public int read() throws IOException {
  2. return read0();
  3. }
  4. private native int read0() throws IOException;
  5. public int read(byte b[]) throws IOException {
  6. return readBytes(b, 0, b.length);
  7. }
  8. public int read(byte b[], int off, int len) throws IOException {
  9. return readBytes(b, off, len);
  10. }
  11. private native int readBytes(byte b[], int off, int len) throws IOException;

可以看出,FileInputStream的三个主要read方法,依赖于两个本地方法,先来看看读取一个字节的read0方法:

  1. // jdk/src/share/native/java/io/FileInputStream.c
  2. JNIEXPORT jint JNICALL
  3. Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {
  4. return readSingle(env, this, fis_fd);
  5. }
  6. // jdk/src/share/native/java/io/io_util.c
  7. jint
  8. readSingle(JNIEnv *env, jobject this, jfieldID fid) {
  9. jint nread;
  10. char ret;
  11. // 获取记录在FileDescriptor中的文件描述符
  12. FD fd = GET_FD(this, fid);
  13. if (fd == -1) {
  14. JNU_ThrowIOException(env, "Stream Closed");
  15. return -1;
  16. }
  17. // 调用IO_Read读取一个字节
  18. nread = IO_Read(fd, &ret, 1);
  19. if (nread == 0) { /* EOF */
  20. return -1;
  21. } else if (nread == -1) { /* error */
  22. JNU_ThrowIOExceptionWithLastError(env, "Read error");
  23. }
  24. return ret & 0xFF;
  25. }
  26. // jdk/src/solaris/native/java/io/io_util_md.h
  27. #define IO_Read handleRead
  28. // jdk/src/solaris/native/java/io/io_util_md.c
  29. ssize_t
  30. handleRead(FD fd, void *buf, jint len)
  31. {
  32. ssize_t result;
  33. // 调用read系统调用读取文件
  34. RESTARTABLE(read(fd, buf, len), result);
  35. return result;
  36. }
  37. // jdk/src/solaris/native/java/io/io_util_md.h
  38. /*
  39. * Retry the operation if it is interrupted
  40. * 如果被中断,则重试的宏
  41. */
  42. #define RESTARTABLE(_cmd, _result) do { \
  43. do { \
  44. _result = _cmd; \
  45. } while((_result == -1) && (errno == EINTR)); \
  46. } while(0)

read的过程并没有使用JVM提供的函数,而是直接使用open系统调用,为什么有这个区别,目前不太清楚。

  1. // jdk/src/share/native/java/io/FileInputStream.c
  2. JNIEXPORT jint JNICALL
  3. Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,
  4. jbyteArray bytes, jint off, jint len) {
  5. return readBytes(env, this, bytes, off, len, fis_fd);
  6. }
  7. // jdk/src/share/native/java/io/io_util.c
  8. /*
  9. * The maximum size of a stack-allocated buffer.
  10. * 栈上能分配的最大buffer大小
  11. */
  12. #define BUF_SIZE 8192
  13. jint
  14. readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
  15. jint off, jint len, jfieldID fid)
  16. {
  17. jint nread;
  18. char stackBuf[BUF_SIZE]; // BUF_SIZE=8192
  19. char *buf = NULL;
  20. FD fd;
  21. // 传入的Java byte数组不能是null
  22. if (IS_NULL(bytes)) {
  23. JNU_ThrowNullPointerException(env, NULL);
  24. return -1;
  25. }
  26. // off,len参数是否越界判断
  27. if (outOfBounds(env, off, len, bytes)) {
  28. JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
  29. return -1;
  30. }
  31. // 如果要读取的长度是0,直接返回读取长度0
  32. if (len == 0) {
  33. return 0;
  34. } else if (len > BUF_SIZE) {
  35. // 如果要读取的长度大于BUF_SIZE,则不能在栈上分配空间了,需要在堆上分配空间
  36. buf = malloc(len);
  37. if (buf == NULL) {
  38. // malloc分配失败,抛出OOM异常
  39. JNU_ThrowOutOfMemoryError(env, NULL);
  40. return 0;
  41. }
  42. } else {
  43. buf = stackBuf;
  44. }
  45. // 获取记录在FileDescriptor中的文件描述符
  46. fd = GET_FD(this, fid);
  47. if (fd == -1) {
  48. JNU_ThrowIOException(env, "Stream Closed");
  49. nread = -1;
  50. } else {
  51. // 调用IO_Read读取
  52. nread = IO_Read(fd, buf, len);
  53. if (nread > 0) {
  54. // 读取成功后,从buf拷贝数据到Java的byte数组中
  55. (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);
  56. } else if (nread == -1) {
  57. // read系统调用返回-1是读取失败
  58. JNU_ThrowIOExceptionWithLastError(env, "Read error");
  59. } else { /* EOF */
  60. // 操作系统read读取返回0认为是读取结束,Java中返回-1认为是读取结束
  61. nread = -1;
  62. }
  63. }
  64. // 如果使用的是堆空间(len > BUF_SIZE),需要手动释放
  65. if (buf != stackBuf) {
  66. free(buf);
  67. }
  68. return nread;
  69. }

FileInputStream#read(byte[], int, int)的主要流程:

  1. 检查参数是否合法(byte数组不能为空,off和len没有越界)
  2. 判断读取的长度,如果等于0直接返回0,如果大于BUF_SIZE需要在堆空间申请内存,如果`0则直接在使用栈空间的缓存
  3. 调用read系统调用读取文件内容到内存中
  4. 从C空间的char数组复制数据到Java空间的byte数组中

重要收获:

  1. 使用FileInputStream#read(byte[], int, int)读取的长度,len一定不能大于8192!因为在小于8192时,会直接利用栈空间的char数组,如果大于,则需要调用malloc申请内存,并且还需要free释放内存,这是非常消耗时间的。
  2. 相比于直接使用系统调用,Java的读取会多一次拷贝!(思考:使用C标准库的fread和Java的read,复制次数是一样,还是fread会少一次?)

移动偏移量

  1. public native long skip(long n) throws IOException;
  2. // jdk/src/share/native/java/io/FileInputStream.c
  3. JNIEXPORT jlong JNICALL
  4. Java_java_io_FileInputStream_skip(JNIEnv *env, jobject this, jlong toSkip) {
  5. jlong cur = jlong_zero;
  6. jlong end = jlong_zero;
  7. // 获取记录在FileDescriptor中的文件描述符
  8. FD fd = GET_FD(this, fis_fd);
  9. if (fd == -1) {
  10. JNU_ThrowIOException (env, "Stream Closed");
  11. return 0;
  12. }
  13. // 调用seek系统调用移动当前偏移量
  14. if ((cur = IO_Lseek(fd, (jlong)0, (jint)SEEK_CUR)) == -1) {
  15. // 获取当前文件偏移量
  16. JNU_ThrowIOExceptionWithLastError(env, "Seek error");
  17. } else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1) {
  18. // 移动偏移量
  19. JNU_ThrowIOExceptionWithLastError(env, "Seek error");
  20. }
  21. return (end - cur);
  22. }
  23. // jdk/src/solaris/native/java/io/io_util_md.h
  24. #ifdef _ALLBSD_SOURCE
  25. #define open64 open
  26. #define fstat64 fstat
  27. #define stat64 stat
  28. #define lseek64 lseek
  29. #define ftruncate64 ftruncate
  30. #define IO_Lseek lseek
  31. #else
  32. #define IO_Lseek lseek64
  33. #endif

获取文件可读取的字节数

  1. public native int available() throws IOException;
  2. // jdk/src/share/native/java/io/FileInputStream.c
  3. JNIEXPORT jint JNICALL
  4. Java_java_io_FileInputStream_available(JNIEnv *env, jobject this) {
  5. jlong ret;
  6. // 获取记录在FileDescriptor中的文件描述符
  7. FD fd = GET_FD(this, fis_fd);
  8. if (fd == -1) {
  9. JNU_ThrowIOException (env, "Stream Closed");
  10. return 0;
  11. }
  12. // 调用IO_Available获取可读字节数
  13. if (IO_Available(fd, &ret)) {
  14. if (ret > INT_MAX) {
  15. ret = (jlong) INT_MAX;
  16. } else if (ret < 0) {
  17. ret = 0;
  18. }
  19. return jlong_to_jint(ret);
  20. }
  21. JNU_ThrowIOExceptionWithLastError(env, NULL);
  22. return 0;
  23. }
  24. // jdk/src/solaris/native/java/io/io_util_md.h
  25. #define IO_Available handleAvailable
  26. // jdk/src/solaris/native/java/io/io_util_md.c
  27. jint
  28. handleAvailable(FD fd, jlong *pbytes)
  29. {
  30. int mode;
  31. struct stat64 buf64;
  32. jlong size = -1, current = -1;
  33. // 获取文件的长度
  34. int result;
  35. RESTARTABLE(fstat64(fd, &buf64), result);
  36. if (result != -1) {
  37. mode = buf64.st_mode;
  38. if (S_ISCHR(mode) || S_ISFIFO(mode) || S_ISSOCK(mode)) {
  39. // 字符特殊文件,管道或FIFO,套接字
  40. int n;
  41. int result;
  42. RESTARTABLE(ioctl(fd, FIONREAD, &n), result);
  43. if (result >= 0) {
  44. *pbytes = n;
  45. return 1;
  46. }
  47. } else if (S_ISREG(mode)) {
  48. // 普通文件,从st_size字段可以直接获取文件大小
  49. size = buf64.st_size;
  50. }
  51. }
  52. // 获取当前文件偏移量
  53. if ((current = lseek64(fd, 0, SEEK_CUR)) == -1) {
  54. return 0;
  55. }
  56. // 如果fstat获取的大小小于当前偏移量,则通过偏移量方式再次获取文件长度
  57. if (size < current) {
  58. if ((size = lseek64(fd, 0, SEEK_END)) == -1)
  59. return 0;
  60. else if (lseek64(fd, current, SEEK_SET) == -1)
  61. return 0;
  62. }
  63. // 文件长度减去当前偏移量得到文件可读长度
  64. *pbytes = size - current;
  65. return 1;
  66. }

关闭文件

  1. public void close() throws IOException {
  2. // 保证只有一个线程会执行关闭逻辑
  3. synchronized (closeLock) {
  4. if (closed) {
  5. return;
  6. }
  7. closed = true;
  8. }
  9. // 关闭关联的Channel
  10. if (channel != null) {
  11. channel.close();
  12. }
  13. // 调用FileDescriptor的closeAll,关闭所有相关流,并调用close系统调用关闭文件描述符
  14. fd.closeAll(new Closeable() {
  15. public void close() throws IOException {
  16. close0();
  17. }
  18. });
  19. }

关闭文件的流程可以参考JDK源码阅读-FileDescriptor

总结

  • FileInputStream打开文件使用open系统调用
  • FileInputStream读取文件使用read系统调用
  • FileInputStream关闭文件使用close系统调用
  • FileInputStream修改文件当前偏移量使用lseek系统调用
  • FileInputStream获取文件可读字节数使用fstat系统调用
  • 使用FileInputStream#read(byte[], int, int)读取的长度,len一定不能大于8192!因为在小于8192时,会直接利用栈空间的char数组,如果大于,则需要调用malloc申请内存,并且还需要free释放内存,这是非常消耗时间的。
  • 相比于直接使用系统调用,Java的读取文件会多一次拷贝!因为使用read读取文件内容到C空间的数组后,需要拷贝数据到JVM的堆空间的数组中
  • FileInputStream#read是无缓冲的,所以每次调用对对应一次系统调用,可能会有较低的性能,需要结合BufferedInputStream提高性能

参考资料

JDK源码阅读-FileInputStream的更多相关文章

  1. JDK源码阅读-FileOutputStream

    本文转载自JDK源码阅读-FileOutputStream 导语 FileOutputStream用户打开文件并获取输出流. 打开文件 public FileOutputStream(File fil ...

  2. JDK源码阅读-RandomAccessFile

    本文转载自JDK源码阅读-RandomAccessFile 导语 FileInputStream只能用于读取文件,FileOutputStream只能用于写入文件,而对于同时读取文件,并且需要随意移动 ...

  3. JDK源码阅读-FileDescriptor

    本文转载自JDK源码阅读-FileDescriptor 导语 操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数.Java虽然在设计上使用了抽象程度更高的流来作为文 ...

  4. JDK源码阅读(三):ArraryList源码解析

    今天来看一下ArrayList的源码 目录 介绍 继承结构 属性 构造方法 add方法 remove方法 修改方法 获取元素 size()方法 isEmpty方法 clear方法 循环数组 1.介绍 ...

  5. JDK源码阅读(一):Object源码分析

    最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...

  6. 利用IDEA搭建JDK源码阅读环境

    利用IDEA搭建JDK源码阅读环境 首先新建一个java基础项目 基础目录 source 源码 test 测试源码和入口 准备JDK源码 下图框起来的路径就是jdk的储存位置 打开jdk目录,找到sr ...

  7. JDK源码阅读-ByteBuffer

    本文转载自JDK源码阅读-ByteBuffer 导语 Buffer是Java NIO中对于缓冲区的封装.在Java BIO中,所有的读写API,都是直接使用byte数组作为缓冲区的,简单直接.但是在J ...

  8. JDK源码阅读-Reference

    本文转载自JDK源码阅读-Reference 导语 Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象. 如果 ...

  9. JDK源码阅读-DirectByteBuffer

    本文转载自JDK源码阅读-DirectByteBuffer 导语 在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计.但是他是一个抽象类,真正的实现分为两类:HeapB ...

随机推荐

  1. Form表单的知识点汇总

    分享学习到的Form知识点,希望给同样有所需要的朋友共同学习..愿我的分享,可以成为您的厚爱.. 简单的知识收到简单的回报,未来的努力造就优秀的自己... <!--<form> -- ...

  2. CSS选择器,属性前缀,长度单位,变形效果,过渡效果,动画效果

    CSS3选择器 ·*通配选择器 ·E标签选择器 ·E#id ID选择器 ·E.class类选择器 ·E F包含选择器,后代选择器 ·E>F子包含选择器 ·E+F相邻兄弟选择器 ·E[foo]属性 ...

  3. springboot中扩展ModelAndView实现net mvc的ActionResult效果

    最近在写spring boot项目,写起来感觉有点繁琐,为了简化spring boot中的Controller开发,对ModelAndView进行简单的扩展,实现net mvc中ActionResul ...

  4. Codeforces Round #594 (Div. 2) D1 - The World Is Just a Programming Task

    思路:枚举换的位置i,j 然后我们要先判断改序列能否完全匹配 如果可以 那我们就需要把差值最大的位置换过来 然后直接判断就行

  5. Java多线程同步和异步问题

    我们首先来说一下多线程: 多线程很形象的例子就是:在一个时刻下,一个班级的学生有人在拖地,有人在擦窗户,有人在擦桌子 按照单线程程序,肯定是先去拖地,再去擦窗户,再去擦桌子.但是在多线程就好像他们在一 ...

  6. 2020 ICPC Asia Taipei-Hsinchu Regional Problem H Optimization for UltraNet (二分,最小生成树,dsu计数)

    题意:给你一张图,要你去边,使其成为一个边数为\(n-1\)的树,同时要求树的最小边权最大,如果最小边权最大的情况有多种,那么要求总边权最小.求生成树后的所有简单路径上的最小边权和. 题解:刚开始想写 ...

  7. UVA - 12295 最短路(迪杰斯特拉)——求按对称路线最短路条数

    题意: 给你一个n,然后给你一个n*n的正方形w[i][j],你需要找到一个从(1,1)点走到(n,n)点的最短路径数量.而且这个路径必须按照y=x对称 题解: 我们把左上角的点当作(0,0)点,右下 ...

  8. Jpress小程序

    首页轮播.首页公告.首页宫格.个人中心页面均支持在PC后台设置内容 首页列表.分类列表页.搜索列表的文章展示页均支持后台设置,拥有三种风格 所有分类展示支持两种风格 用户中心授权登陆,查看个人数据 J ...

  9. DCL 数据控制语言

    目录 授予权限(GRANT) 回收权限(REVOTE) 授予权限(GRANT) # 语法 mysql> help grant; Name: 'GRANT' Description: Syntax ...

  10. Hexo、主题、部署上线

    Hexo.主题.部署上线 安装Hexo git和nodejs安装好后,就可以安装hexo了,你可以先创建一个文件夹MyBlog,用来存放自己的博客文件,然后cd到这个文件夹下(或者在这个文件夹下直接右 ...