直接读文件频繁触发磁盘io是因为逐字节读取导致每次调用都引发系统调用和磁盘寻道;bufferedinputstream通过8kb~32kb缓冲区合并读操作,配合read(byte[])才能显著减少io次数。

为什么直接读文件会频繁触发磁盘 IO
当用 FileInputStream.read() 逐字节或小缓冲区(比如 1 字节)读取大文件时,每次调用都可能触发一次系统调用,进而引发实际磁盘寻道和读块操作。现代磁盘(尤其是机械盘)随机访问延迟高,频繁小读效率极低。
根本问题不是 Java 层慢,而是没把「内核态数据搬运」这件事批量做——BufferedInputStream 的作用就是把多次小读合并成一次较大系统调用,再在用户态内存里分发。
BufferedInputStream 的缓冲区大小怎么设才有效
默认构造函数用 8192 字节(8KB),对大多数场景够用,但并非越大越好:
- 缓冲区过小(如
new BufferedInputStream(in, 256))无法覆盖典型文件系统块大小(通常 4KB),仍频繁陷入内核 - 缓冲区过大(如 1MB)会浪费堆内存,且单次系统调用未必更快(内核有自身优化阈值,一般 64KB~128KB 已接近吞吐上限)
- 实测表明:本地 SSD 上,8KB–64KB 区间性能差异很小;机械盘上 32KB 可比 8KB 提升约 10%~15%
推荐做法:new BufferedInputStream(in, 32 * 1024) —— 明确指定 32KB,在内存可控前提下更贴近底层设备特性。
必须配合 read(byte[]) 才能真正减少系统调用
BufferedInputStream.read()(单字节重载)虽然也走缓冲,但每次仍需检查缓冲区是否耗尽、是否需要 refill,开销集中在 Java 层;而真正省 IO 的关键是让每次 read(byte[]) 尽量填满缓冲区,减少 refill 次数。
错误写法:
int b;
while ((b = bis.read()) != -1) { /* 每次只读 1 字节 */ }
正确写法:
byte[] buf = new byte[8192];
int n;
while ((n = bis.read(buf)) != -1) {
// 处理 buf[0] 到 buf[n-1]
}
注意:bis.read(buf) 会优先从内部缓冲区拷贝,仅当缓冲区空了才触发底层 in.read() 填充——这才是减少磁盘 IO 的关键路径。
关闭流时别漏掉 flush,但 BufferedInputStream 本身不支持 flush
BufferedInputStream 是只读装饰器,没有 flush() 方法,也不缓存写入数据。这点常被混淆——如果你看到“IO 未生效”,大概率是误以为它像 BufferedOutputStream 那样需要 flush。
真正要小心的是:
- 忘记调用
bis.close()→ 底层FileInputStream句柄泄露,可能触发“Too many open files” - 用 try-with-resources 时,确保原始流没被提前关闭(比如包装前就 close 了)
- 如果后续还要读同一文件,别复用已 close 的
BufferedInputStream,会抛IOException: Stream closed
缓冲效果是否生效,最直接的验证方式是用 strace -e trace=read java YourApp 观察系统调用次数——加了 BufferedInputStream 后,read() 系统调用应减少一个数量级。











