FileIntputStream用于打开一个文件并获取输入流。
打开文件
我们来看看FileIntputStream打开文件时,做了什么操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | public FileInputStream(File file) throws FileNotFoundException {     String name = (file != null ? file.getPath() : null);     SecurityManager security = System.getSecurityManager();     if (security != null) {         security.checkRead(name);     }     if (name == null) {         throw new NullPointerException();     }     if (file.isInvalid()) {         throw new FileNotFoundException("Invalid file path");     }     fd = new FileDescriptor();     fd.attach(this);     path = name;     open(name); }
  private void open(String name) throws FileNotFoundException {     open0(name); }
  private native void open0(String name) throws FileNotFoundException;
  | 
 
FileIntputStream的构造函数,在Java层面做的事情不多:
- 检查是否有读取文件的权限
 
- 判断文件路径是否合法
 
- 新建
FileDescriptor实例 
- 调用
open0本地方法 
FileDescriptor类对应操作系统的文件描述符,具体可以参考JDK源码阅读-FileDescriptor这篇文章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
   |  JNIEXPORT void JNICALL Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {          fileOpen(env, this, path, fis_fd, O_RDONLY); }
 
  void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags) {     WITH_PLATFORM_STRING(env, path, ps) {         FD fd;
  #if defined(__linux__) || defined(_ALLBSD_SOURCE)                  char *p = (char *)ps + strlen(ps) - 1;         while ((p > ps) && (*p == '/'))             *p-- = '\0'; #endif         fd = JVM_Open(ps, flags, 0666);          if (fd >= 0) {             SET_FD(this, fd, fid);          } else {             throwFileNotFoundException(env, path);           }     } END_PLATFORM_STRING(env, ps); }
 
  | 
 
FileOutputStream#open的JNI代码逻辑也比较简单:
- 如果是Linux或BSD,去掉path结尾的/,因为这些内核不需要
 
- 调用
JVM_Open函数打开文件,得到文件描述符 
- 调用
SET_FD设置文件描述符到FileDescriptor#fd 
SET_FD用于设置文件描述符到FileDescriptor#fd,具体可以参考JDK源码阅读-FileDescriptor这篇文章。
JVM_Open根据其命名可以看得出来是JVM提供的函数,可以看出JDK的实现是分为多层的:Java-JNI-JDK,需要和操作系统交互的代码在JNI层面,一些每个操作系统都需要提供的真正底层的方法JVM来提供。具体的这个分层设计以后如果能有机会看JVM实现应该能有更深的理解。
JVM_Open的实现可以在Hotspot虚拟机的代码中找到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
   |  JVM_LEAF(jint, JVM_Open(const char *fname, jint flags, jint mode))   JVMWrapper2("JVM_Open (%s)", fname);
       int result = os::open(fname, flags, mode);     if (result >= 0) {     return result;   } else {     switch(errno) {       case EEXIST:         return JVM_EEXIST;       default:         return -1;     }   } JVM_END
 
  int os::open(const char *path, int oflag, int mode) {
       if (strlen(path) > MAX_PATH - 1) {     errno = ENAMETOOLONG;     return -1;   }   int fd;      int o_delete = (oflag & O_DELETE);   oflag = oflag & ~O_DELETE;
       fd = ::open64(path, oflag, mode);   if (fd == -1) return -1;
       {     struct stat64 buf64;     int ret = ::fstat64(fd, &buf64);     int st_mode = buf64.st_mode;
      if (ret != -1) {       if ((st_mode & S_IFMT) == S_IFDIR) {         errno = EISDIR;         ::close(fd);         return -1;       }     } else {       ::close(fd);       return -1;     }   }
  #ifdef FD_CLOEXEC                    {         int flags = ::fcntl(fd, F_GETFD);         if (flags != -1)             ::fcntl(fd, F_SETFD, flags | FD_CLOEXEC);     } #endif
    if (o_delete != 0) {     ::unlink(path);   }   return fd; }
 
  | 
 
可以看到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
这样完整的打开文件流程就分析完了,去掉各种函数调用,本质上只做了两件事:
- 调用
open系统调用打开文件 
- 保存得到的文件描述符到
FileDescriptor#fd中 
读取文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | public int read() throws IOException {     return read0(); }
  private native int read0() 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); }
  private native int readBytes(byte b[], int off, int len) throws IOException;
  | 
 
可以看出,FileInputStream的三个主要read方法,依赖于两个本地方法,先来看看读取一个字节的read0方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
   |  JNIEXPORT jint JNICALL Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {     return readSingle(env, this, fis_fd); }
 
  jint readSingle(JNIEnv *env, jobject this, jfieldID fid) {     jint nread;     char ret;
           FD fd = GET_FD(this, fid);     if (fd == -1) {         JNU_ThrowIOException(env, "Stream Closed");         return -1;     }
           nread = IO_Read(fd, &ret, 1);     if (nread == 0) {          return -1;     } else if (nread == -1) {          JNU_ThrowIOExceptionWithLastError(env, "Read error");     }     return ret & 0xFF; }
 
  #define IO_Read handleRead
 
  ssize_t handleRead(FD fd, void *buf, jint len) {     ssize_t result;          RESTARTABLE(read(fd, buf, len), result);     return result; }
 
 
 
 
 
  #define RESTARTABLE(_cmd, _result) do { \     do { \         _result = _cmd; \     } while((_result == -1) && (errno == EINTR)); \ } while(0)
 
  | 
 
read的过程并没有使用JVM提供的函数,而是直接使用open系统调用,为什么有这个区别,目前不太清楚。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
   |  JNIEXPORT jint JNICALL Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,         jbyteArray bytes, jint off, jint len) {     return readBytes(env, this, bytes, off, len, fis_fd); }
 
 
 
 
 
  #define BUF_SIZE 8192
  jint readBytes(JNIEnv *env, jobject this, jbyteArray bytes,           jint off, jint len, jfieldID fid) {     jint nread;     char stackBuf[BUF_SIZE];      char *buf = NULL;     FD fd;
           if (IS_NULL(bytes)) {         JNU_ThrowNullPointerException(env, NULL);         return -1;     }          if (outOfBounds(env, off, len, bytes)) {         JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);         return -1;     }
           if (len == 0) {         return 0;     } else if (len > BUF_SIZE) {                  buf = malloc(len);         if (buf == NULL) {                          JNU_ThrowOutOfMemoryError(env, NULL);             return 0;         }     } else {         buf = stackBuf;     }
           fd = GET_FD(this, fid);     if (fd == -1) {         JNU_ThrowIOException(env, "Stream Closed");         nread = -1;     } else {                  nread = IO_Read(fd, buf, len);         if (nread > 0) {                          (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);         } else if (nread == -1) {                          JNU_ThrowIOExceptionWithLastError(env, "Read error");         } else {                           nread = -1;         }     }
           if (buf != stackBuf) {         free(buf);     }     return nread; }
 
  | 
 
FileInputStream#read(byte[], int, int)的主要流程:
- 检查参数是否合法(byte数组不能为空,off和len没有越界)
 
- 判断读取的长度,如果等于0直接返回0,如果大于BUF_SIZE需要在堆空间申请内存,如果
0<len<=BUF_SIZE则直接在使用栈空间的缓存 
- 调用
read系统调用读取文件内容到内存中 
- 从C空间的char数组复制数据到Java空间的byte数组中
 
重要收获:
- 使用
FileInputStream#read(byte[], int, int)读取的长度,len一定不能大于8192!因为在小于8192时,会直接利用栈空间的char数组,如果大于,则需要调用malloc申请内存,并且还需要free释放内存,这是非常消耗时间的。 
- 相比于直接使用系统调用,Java的读取会多一次拷贝!(思考:使用C标准库的fread和Java的read,复制次数是一样,还是fread会少一次?)
 
移动偏移量
1
   | public native long skip(long n) throws IOException;
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
   |  JNIEXPORT jlong JNICALL Java_java_io_FileInputStream_skip(JNIEnv *env, jobject this, jlong toSkip) {     jlong cur = jlong_zero;     jlong end = jlong_zero;
           FD fd = GET_FD(this, fis_fd);     if (fd == -1) {         JNU_ThrowIOException (env, "Stream Closed");         return 0;     }
           if ((cur = IO_Lseek(fd, (jlong)0, (jint)SEEK_CUR)) == -1) {                  JNU_ThrowIOExceptionWithLastError(env, "Seek error");     } else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1) {                  JNU_ThrowIOExceptionWithLastError(env, "Seek error");     }     return (end - cur); }
 
  #ifdef _ALLBSD_SOURCE #define open64 open #define fstat64 fstat #define stat64 stat #define lseek64 lseek #define ftruncate64 ftruncate #define IO_Lseek lseek #else #define IO_Lseek lseek64 #endif
 
  | 
 
获取文件可读取的字节数
1
   | public native int available() throws IOException;
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
   |  JNIEXPORT jint JNICALL Java_java_io_FileInputStream_available(JNIEnv *env, jobject this) {     jlong ret;          FD fd = GET_FD(this, fis_fd);     if (fd == -1) {         JNU_ThrowIOException (env, "Stream Closed");         return 0;     }          if (IO_Available(fd, &ret)) {         if (ret > INT_MAX) {             ret = (jlong) INT_MAX;         } else if (ret < 0) {             ret = 0;         }         return jlong_to_jint(ret);     }     JNU_ThrowIOExceptionWithLastError(env, NULL);     return 0; }
 
  #define IO_Available handleAvailable
 
  jint handleAvailable(FD fd, jlong *pbytes) {     int mode;     struct stat64 buf64;     jlong size = -1, current = -1;
           int result;     RESTARTABLE(fstat64(fd, &buf64), result);     if (result != -1) {         mode = buf64.st_mode;         if (S_ISCHR(mode) || S_ISFIFO(mode) || S_ISSOCK(mode)) {                          int n;             int result;             RESTARTABLE(ioctl(fd, FIONREAD, &n), result);             if (result >= 0) {                 *pbytes = n;                 return 1;             }         } else if (S_ISREG(mode)) {                          size = buf64.st_size;         }     }
           if ((current = lseek64(fd, 0, SEEK_CUR)) == -1) {         return 0;     }
           if (size < current) {         if ((size = lseek64(fd, 0, SEEK_END)) == -1)             return 0;         else if (lseek64(fd, current, SEEK_SET) == -1)             return 0;     }
           *pbytes = size - current;     return 1; }
 
  | 
 
关闭文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | public void close() throws IOException {     // 保证只有一个线程会执行关闭逻辑     synchronized (closeLock) {         if (closed) {             return;         }         closed = true;     }     // 关闭关联的Channel     if (channel != null) {         channel.close();     }
      // 调用FileDescriptor的closeAll,关闭所有相关流,并调用close系统调用关闭文件描述符     fd.closeAll(new Closeable() {         public void close() throws IOException {             close0();         }     }); }
  | 
 
关闭文件的流程可以参考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提高性能 
参考资料