FileInputStream / FileChannel 文件 IO 源码精读
概述
Java 的文件读写最终都落在系统调用上:FileInputStream/FileOutputStream 走 JVM native 层逐字节/逐块读写;FileChannel 提供更高效的通道抽象,支持零拷贝(transferTo)、内存映射(mmap)与文件锁。理解 native 调用链与系统调用语义,才能明白"为什么 FileChannel 更快"。本文基于 OpenJDK 21 源码拆解。
一、FileInputStream 的 native 读取
1.1 read() 的调用链
java
// java.io.FileInputStream
public int read() throws IOException {
return read0(); // native 方法
}
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len); // native 批量读
}
private native int read0() throws IOException;
private native int readBytes(byte b[], int off, int len) throws IOException;native 实现(OpenJDK FileInputStream.c → io_util.c):
read0() → JVM 层调用:
Linux → read(fd, buf, 1) // 每次读 1 字节
Windows → ReadFile(fd, buf, 1, ...)
readBytes(b, off, len) → 循环系统调用:
Linux → read(fd, buf + off, len)
Windows → ReadFile(fd, buf + off, len)逐字节读的性能问题:
read()每次调用都进一次内核(系统调用开销大)。所以业务上应使用BufferedInputStream或read(byte[], off, len)批量读。
1.2 FileInputStream(FileDescriptor) 的 fd 管理
java
public FileInputStream(FileDescriptor fdObj) {
// 安全检查
SecurityManager security = System.getSecurityManager();
if (security != null) { ... }
fd = fdObj; // 直接绑定外部 fd
fd.incrementAndGetUseCount(); // 引用计数 +1(close 时减 1)
}
public void close() throws IOException {
// 引用计数 > 0 才真正关闭底层 fd
if (fd.decrementAndGetUseCount() == 0)
close0(); // native close(fd)
}java
// java.io.FileDescriptor
public boolean valid() { return fd != -1; } // 校验 fd 有效性关键点:
fd 使用计数(useCount):
FileInputStream(fd) 与 FileOutputStream(fd) 可能共享同一 fd
谁 close 都不该真正关闭底层 fd,除非使用计数归零
→ incrementAndGetUseCount / decrementAndGetUseCount 守护二、FileChannel 的通道读取
2.1 read(ByteBuffer) 的实现
java
// sun.nio.ch.FileChannelImpl
public int read(ByteBuffer dst) throws IOException {
ensureOpen();
...
return IOUtil.read(fd, dst, -1, nd); // native 辅助类
}
// sun.nio.ch.IOUtil
static int read(FileDescriptor fd, ByteBuffer dst, long position, NativeDispatcher nd) {
if (dst.isDirect()) {
return readIntoNativeBuffer(fd, dst, position, nd); // 直接缓冲区直读
}
// 堆缓冲区:临时分配 direct buffer 中转,避免 JNI 访问堆数组
ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
try {
int n = readIntoNativeBuffer(fd, bb, position, nd);
// 拷贝回堆缓冲区
bb.flip();
if (n > 0) dst.put(bb);
return n;
} finally {
Util.offerFirstTemporaryDirectBuffer(bb); // 临时缓冲区回收复用
}
}native 层最终调用 pread0:
pread0(fd, address, len, position):
Linux → pread(fd, buf, len, pos) // 带偏移的原子读(不移动文件指针)
Windows → ReadFile(fd, buf, len, ...) + OVERLAPPED 偏移堆缓冲区(
HeapByteBuffer)要经过临时 direct buffer 中转:因为 JNI 层直接访问堆数组有 GC 移动对象的风险,先拷贝到固定地址的直接内存再系统调用。
2.2 文件位置与大小
java
public FileChannel position(long newPosition) throws IOException {
// position0() native → lseek(fd, newPosition, SEEK_SET)
}
public long size() throws IOException {
// fstat(fd) 获取文件大小
}三、transferTo() 零拷贝
java
// sun.nio.ch.FileChannelImpl.transferTo
public long transferTo(long position, long count, WritableByteChannel target) {
...
// 尝试三种路径:
// ① transferToDirectly:sendfile/TransmitFile 内核级零拷贝
// ② transferToTrustedChannel:目标通道也是文件通道 → 用 mmap 映射
// ③ transferToArbitraryChannel:普通通道 → 缓冲循环(走 heap buffer)
}零拷贝核心(transferToDirectly):
Linux → sendfile(out_fd, in_fd, offset, count)
(内核直接在两个 fd 间搬数据,不经用户态)
Windows → TransmitFile(hSocket, hFile, ...)
非零拷贝路径(兜底):
read 到用户态 buffer → write 到目标通道(两次系统调用 + 用户态拷贝)零拷贝的意义:
传统方式:磁盘 → 内核缓冲 → 用户缓冲 → 内核 Socket 缓冲 → 网卡(4 次拷贝)
零拷贝: 磁盘 → 内核缓冲 → 网卡(2 次以内,DMA 完成)
→ 减少 CPU 拷贝,吞吐显著提升(大文件传输场景)四、map() 内存映射文件
java
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
// 合法性校验:位置、大小、模式(只读/读写/私有 COW)
...
long addr = -1;
if (size > 0) {
addr = map0(mode, position, size); // native mmap
}
...
// 根据模式创建不同映射缓冲区(都继承 MappedByteBuffer)
if (mode == MapMode.READ_ONLY)
return Util.newMappedByteBufferR(size, addr + pagePosition, fd);
else
return Util.newMappedByteBuffer(size, addr + pagePosition, fd);
}map0() → mmap 系统调用:
Linux → mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset)
Windows → MapViewOfFile(hFileMapping, ...)
返回值:
映射后的虚拟地址 addr → 包装成 DirectByteBuffer(address 字段指向该内存)
→ 读写该 buffer 直接操作文件页缓存(缺页时内核按需加载)java
// 用法示例:大文件随机访问
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
MappedByteBuffer mbb = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
byte b = mbb.get(1024 * 1024 * 100); // 随机读,无需 seekmmap 与普通 read 的区别:
普通 read:每次系统调用拷贝到用户缓冲(page cache → 用户态)
mmap:直接把文件页映射进进程地址空间(缺页时内核加载)
→ 随机访问/多次读取场景优势大;顺序小读场景优势不明显五、MappedByteBuffer.force() 与文件锁
5.1 force() 的 msync
java
public final MappedByteBuffer force() {
if (fd != null) { ... }
force0(fd, address, capacity); // native:把映射区域刷回磁盘
return this;
}
// force0() → msync:
// Linux → msync(addr, len, MS_SYNC)
// Windows → FlushViewOfFileforce() 保证映射缓冲区修改同步落盘(写回文件),类似 FileChannel.force / FileOutputStream.getFD().sync(),用于需要持久性保证的场景。
5.2 FileLock.lock() 的跨进程文件锁
java
// sun.nio.ch.FileChannelImpl.lock
public FileLock lock(long position, long size, boolean shared) throws IOException {
// 尝试非阻塞 lock0,失败则循环重试(阻塞语义)
...
}
// lock0() 系统调用:
// Linux → fcntl(fd, F_SETLK / F_SETLKW, ...) 或 flock
// Windows → LockFileEx(fd, flags, ..., overlapped)文件锁语义:
锁范围:文件区间 [position, position + size),可部分加锁
模式:共享锁(shared,多读者)/ 独占锁(exclusive,写者)
作用域:跨进程(不是线程锁)
注意:不同进程间文件锁生效;同一 JVM 内 JVM 会抛异常而非排队六、RandomAccessFile 的 seek
java
public void seek(long pos) throws IOException {
...
seek0(pos); // native
// 同时更新内部的文件指针缓存
}
// seek0() → lseek:
// Linux → lseek(fd, pos, SEEK_SET)
// Windows → SetFilePointer(fd, pos, ...)RandomAccessFile 是"带指针的文件流",seek 移动指针后 read/write 从新位置继续。与 FileChannel.position() 等价,但更偏流式 API。
java
// 用法示例:按随机位置读写
RandomAccessFile raf = new RandomAccessFile("data.bin", "rw");
raf.seek(1024); // 跳到 1024 字节处
raf.writeInt(42); // 写入 4 字节
raf.close();七、实现要点
文件 IO 核心:
FileInputStream:read0/readBytes native → read/ReadFile 系统调用
fd 管理:FileDescriptor.valid() + useCount 引用计数
FileChannel.read:IOUtil 中转,堆缓冲走临时 direct buffer
transferTo:sendfile/TransmitFile 零拷贝(内核态直接传输)
map:mmap 内存映射 → DirectByteBuffer(address)
force:msync 同步落盘
FileLock:fcntl/LockFileEx 跨进程文件锁
RandomAccessFile:lseek/SetFilePointer 移动指针
常见陷阱:
逐字节 read() 性能差 → 用批量读或缓冲流
堆缓冲每次系统调用前要中转 direct buffer
mmap 不释放 → 直到 buffer 被 GC(DirectByteBuffer cleaner 回收)
文件锁是跨进程锁,不防同进程内竞争
transferTo 对小文件收益有限(系统调用开销占比大)