JDK源码阅读-FileInputStream
导语
FileIntputStream
用于打开一个文件并获取输入流。
打开文件
我们来看看FileIntputStream
打开文件时,做了什么操作:
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这篇文章。
// jdk/src/share/native/java/io/FileInputStream.c
JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
// 使用O_RDONLY只读模式打开文件
fileOpen(env, this, path, fis_fd, O_RDONLY);
}
// jdk/src/solaris/native/java/io/io_util_md.c
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)
// 如果是Linux或BSD,去掉path结尾的/,因为这些内核不需要
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); // 非负整数认为是正确的文件描述符,设置到fd字段
} else {
throwFileNotFoundException(env, path); // 负数认为是不正确文件描述符,抛出FileNotFoundException异常
}
} 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虚拟机的代码中找到:
// hotspot/src/share/vm/prims/jvm.cpp
JVM_LEAF(jint, JVM_Open(const char *fname, jint flags, jint mode))
JVMWrapper2("JVM_Open (%s)", fname);
//%note jvm_r6
int result = os::open(fname, flags, mode); // 调用os::open打开文件
if (result >= 0) {
return result;
} else {
switch(errno) {
case EEXIST:
return JVM_EEXIST;
default:
return -1;
}
}
JVM_END
// hotspot/src/os/linux/vm/os_linux.cpp
int os::open(const char *path, int oflag, int mode) {
// 如果path长度大于MAX_PATH,抛出异常
if (strlen(path) > MAX_PATH - 1) {
errno = ENAMETOOLONG;
return -1;
}
int fd;
// O_DELETE是JVM自定义的一个flag,要在传递给操作系统前去掉
int o_delete = (oflag & O_DELETE);
oflag = oflag & ~O_DELETE;
// 调用open64打开文件
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
// 设置文件描述符标志FD_CLOEXEC
// 这样在fork和exec时,子进程就不会收到父进程打开的文件描述符的影响
// 具体参考[FD_CLOEXEC用法及原因_转](https://www.cnblogs.com/embedded-linux/p/6753617.html)
{
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
中
读取文件
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
方法:
// jdk/src/share/native/java/io/FileInputStream.c
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {
return readSingle(env, this, fis_fd);
}
// jdk/src/share/native/java/io/io_util.c
jint
readSingle(JNIEnv *env, jobject this, jfieldID fid) {
jint nread;
char ret;
// 获取记录在FileDescriptor中的文件描述符
FD fd = GET_FD(this, fid);
if (fd == -1) {
JNU_ThrowIOException(env, "Stream Closed");
return -1;
}
// 调用IO_Read读取一个字节
nread = IO_Read(fd, &ret, 1);
if (nread == 0) { /* EOF */
return -1;
} else if (nread == -1) { /* error */
JNU_ThrowIOExceptionWithLastError(env, "Read error");
}
return ret & 0xFF;
}
// jdk/src/solaris/native/java/io/io_util_md.h
#define IO_Read handleRead
// jdk/src/solaris/native/java/io/io_util_md.c
ssize_t
handleRead(FD fd, void *buf, jint len)
{
ssize_t result;
// 调用read系统调用读取文件
RESTARTABLE(read(fd, buf, len), result);
return result;
}
// jdk/src/solaris/native/java/io/io_util_md.h
/*
* Retry the operation if it is interrupted
* 如果被中断,则重试的宏
*/
#define RESTARTABLE(_cmd, _result) do { \
do { \
_result = _cmd; \
} while((_result == -1) && (errno == EINTR)); \
} while(0)
read的过程并没有使用JVM提供的函数,而是直接使用open系统调用,为什么有这个区别,目前不太清楚。
// jdk/src/share/native/java/io/FileInputStream.c
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);
}
// jdk/src/share/native/java/io/io_util.c
/*
* The maximum size of a stack-allocated buffer.
* 栈上能分配的最大buffer大小
*/
#define BUF_SIZE 8192
jint
readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
jint off, jint len, jfieldID fid)
{
jint nread;
char stackBuf[BUF_SIZE]; // BUF_SIZE=8192
char *buf = NULL;
FD fd;
// 传入的Java byte数组不能是null
if (IS_NULL(bytes)) {
JNU_ThrowNullPointerException(env, NULL);
return -1;
}
// off,len参数是否越界判断
if (outOfBounds(env, off, len, bytes)) {
JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
return -1;
}
// 如果要读取的长度是0,直接返回读取长度0
if (len == 0) {
return 0;
} else if (len > BUF_SIZE) {
// 如果要读取的长度大于BUF_SIZE,则不能在栈上分配空间了,需要在堆上分配空间
buf = malloc(len);
if (buf == NULL) {
// malloc分配失败,抛出OOM异常
JNU_ThrowOutOfMemoryError(env, NULL);
return 0;
}
} else {
buf = stackBuf;
}
// 获取记录在FileDescriptor中的文件描述符
fd = GET_FD(this, fid);
if (fd == -1) {
JNU_ThrowIOException(env, "Stream Closed");
nread = -1;
} else {
// 调用IO_Read读取
nread = IO_Read(fd, buf, len);
if (nread > 0) {
// 读取成功后,从buf拷贝数据到Java的byte数组中
(*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);
} else if (nread == -1) {
// read系统调用返回-1是读取失败
JNU_ThrowIOExceptionWithLastError(env, "Read error");
} else { /* EOF */
// 操作系统read读取返回0认为是读取结束,Java中返回-1认为是读取结束
nread = -1;
}
}
// 如果使用的是堆空间(len > BUF_SIZE),需要手动释放
if (buf != stackBuf) {
free(buf);
}
return nread;
}
FileInputStream#read(byte[], int, int)
的主要流程:
- 检查参数是否合法(byte数组不能为空,off和len没有越界)
- 判断读取的长度,如果等于0直接返回0,如果大于BUF_SIZE需要在堆空间申请内存,如果`0则直接在使用栈空间的缓存
- 调用
read
系统调用读取文件内容到内存中 - 从C空间的char数组复制数据到Java空间的byte数组中
重要收获:
- 使用
FileInputStream#read(byte[], int, int)
读取的长度,len一定不能大于8192!因为在小于8192时,会直接利用栈空间的char数组,如果大于,则需要调用malloc申请内存,并且还需要free释放内存,这是非常消耗时间的。 - 相比于直接使用系统调用,Java的读取会多一次拷贝!(思考:使用C标准库的fread和Java的read,复制次数是一样,还是fread会少一次?)
移动偏移量
public native long skip(long n) throws IOException;
// jdk/src/share/native/java/io/FileInputStream.c
JNIEXPORT jlong JNICALL
Java_java_io_FileInputStream_skip(JNIEnv *env, jobject this, jlong toSkip) {
jlong cur = jlong_zero;
jlong end = jlong_zero;
// 获取记录在FileDescriptor中的文件描述符
FD fd = GET_FD(this, fis_fd);
if (fd == -1) {
JNU_ThrowIOException (env, "Stream Closed");
return 0;
}
// 调用seek系统调用移动当前偏移量
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);
}
// jdk/src/solaris/native/java/io/io_util_md.h
#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
获取文件可读取的字节数
public native int available() throws IOException;
// jdk/src/share/native/java/io/FileInputStream.c
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_available(JNIEnv *env, jobject this) {
jlong ret;
// 获取记录在FileDescriptor中的文件描述符
FD fd = GET_FD(this, fis_fd);
if (fd == -1) {
JNU_ThrowIOException (env, "Stream Closed");
return 0;
}
// 调用IO_Available获取可读字节数
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;
}
// jdk/src/solaris/native/java/io/io_util_md.h
#define IO_Available handleAvailable
// jdk/src/solaris/native/java/io/io_util_md.c
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)) {
// 字符特殊文件,管道或FIFO,套接字
int n;
int result;
RESTARTABLE(ioctl(fd, FIONREAD, &n), result);
if (result >= 0) {
*pbytes = n;
return 1;
}
} else if (S_ISREG(mode)) {
// 普通文件,从st_size字段可以直接获取文件大小
size = buf64.st_size;
}
}
// 获取当前文件偏移量
if ((current = lseek64(fd, 0, SEEK_CUR)) == -1) {
return 0;
}
// 如果fstat获取的大小小于当前偏移量,则通过偏移量方式再次获取文件长度
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;
}
关闭文件
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
提高性能
参考资料
- JDK源码阅读-FileDescriptor
- FD_CLOEXEC用法及原因_转
- man page for open64 (all section 2) - Unix & Linux Commands
- c - Wrapper for open() and open64() and see that system calls by vi uses open64() - Stack Overflow
JDK源码阅读-FileInputStream的更多相关文章
- JDK源码阅读-FileOutputStream
本文转载自JDK源码阅读-FileOutputStream 导语 FileOutputStream用户打开文件并获取输出流. 打开文件 public FileOutputStream(File fil ...
- JDK源码阅读-RandomAccessFile
本文转载自JDK源码阅读-RandomAccessFile 导语 FileInputStream只能用于读取文件,FileOutputStream只能用于写入文件,而对于同时读取文件,并且需要随意移动 ...
- JDK源码阅读-FileDescriptor
本文转载自JDK源码阅读-FileDescriptor 导语 操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数.Java虽然在设计上使用了抽象程度更高的流来作为文 ...
- JDK源码阅读(三):ArraryList源码解析
今天来看一下ArrayList的源码 目录 介绍 继承结构 属性 构造方法 add方法 remove方法 修改方法 获取元素 size()方法 isEmpty方法 clear方法 循环数组 1.介绍 ...
- JDK源码阅读(一):Object源码分析
最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...
- 利用IDEA搭建JDK源码阅读环境
利用IDEA搭建JDK源码阅读环境 首先新建一个java基础项目 基础目录 source 源码 test 测试源码和入口 准备JDK源码 下图框起来的路径就是jdk的储存位置 打开jdk目录,找到sr ...
- JDK源码阅读-ByteBuffer
本文转载自JDK源码阅读-ByteBuffer 导语 Buffer是Java NIO中对于缓冲区的封装.在Java BIO中,所有的读写API,都是直接使用byte数组作为缓冲区的,简单直接.但是在J ...
- JDK源码阅读-Reference
本文转载自JDK源码阅读-Reference 导语 Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象. 如果 ...
- JDK源码阅读-DirectByteBuffer
本文转载自JDK源码阅读-DirectByteBuffer 导语 在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计.但是他是一个抽象类,真正的实现分为两类:HeapB ...
随机推荐
- 从ReentrantLock实现非公平锁的源码理解AQS中的CLH队列
虽然前面也看过AQS的文章,并且转载过一篇大佬的分析,但是我觉得他们对于AQS和ReentrantLock部分的源码的分析并不详细,自己理解期来还是有问题,于是自己准备花时间重新梳理下,好了,进入正题 ...
- Jenkins(8)构建触发器之定时构建和轮询 SCM
前言 跑自动化用例每次用手工点击jenkins出发自动化用例太麻烦了,我们希望能每天固定时间跑,这样就不用管了,坐等收测试报告结果就行. jenkins的定时任务是用的crontab语法 定时构建语法 ...
- ACwing 258. 石头剪子布
258. 石头剪子布 题目传送门 题意挺好理解,但是当我看样例的时候就傻了.不是说好的只有一个裁判的吗?出现矛盾的时候该怎么判定裁判? 分析 观察这个数据量就会发觉是有猫腻的,直接从正面求出裁判并不是 ...
- CF 1244 C - The Football Season
C - The Football Season 先考虑求解 \[ x\times w + y\times d=p \] 若存在一组解 \[ \begin{cases} x_0\\ y_0 = kw + ...
- Codeforces Round #671 (Div. 2)
比赛链接:https://codeforces.com/contest/1419 A. Digit Game 题意 给出一个 $n$ 位数,游戏规则如下: 1-indexed Raze标记奇数位 Br ...
- 最短路径问题---Floyed(弗洛伊德算法),dijkstra算法,SPFA算法
在NOIP比赛中,如果出图论题最短路径应该是个常考点. 求解最短路径常用的算法有:Floyed算法(O(n^3)的暴力算法,在比赛中大概能过三十分) dijkstra算法 (堆优化之后是O(MlogE ...
- POJ - 3693 Maximum repetition substring(重复次数最多的连续重复子串)
传送门:POJ - 3693 题意:给你一个字符串,求重复次数最多的连续重复子串,如果有一样的,取字典序小的字符串. 题解: 比较容易理解的部分就是枚举长度为L,然后看长度为L的字符串最多连续出现 ...
- Strategic game POJ - 1463 树型dp
//题意:就是你需要派最少的士兵来巡查每一条边.相当于求最少点覆盖,用最少的点将所有边都覆盖掉//题解://因为这是一棵树,所以对于每一条边的两个端点,肯定要至少有一个点需要放入士兵,那么对于x-&g ...
- hdu5289 Assignment
Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others) Total Submission ...
- Yocto项目介绍及入门 -- 嵌入师工程师必备利器
目录 写在前面 1. Yocto项目是什么 2. Yocto项目有什么用 3. 如何快速上手Yocto项目 4. 带你通过Yocto项目编译一个自定义镜像文件 写在前面 博主目前从事BMC工作,由于公 ...