IO性能优化
IO 性能优化不能只看 Java 代码。真正的瓶颈可能在磁盘、网络、文件系统、操作系统页缓存、线程模型、序列化、业务处理或下游服务。
# 1. 先定位瓶颈
优化前先回答几个问题:
- 是读慢还是写慢?
- 是磁盘慢还是网络慢?
- 是 IO 慢还是业务处理慢?
- 是吞吐不足还是延迟过高?
- 是单请求慢还是并发下慢?
- GC 是否影响 IO 缓冲和吞吐?
没有测量就优化,很容易把代码写复杂却没有收益。
# 2. 使用缓冲
最常见优化是减少小块 IO。
try (InputStream input = new BufferedInputStream(Files.newInputStream(source));
OutputStream output = new BufferedOutputStream(Files.newOutputStream(target))) {
byte[] buffer = new byte[64 * 1024];
int len;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
}
缓冲区不是越大越好,要结合压测。
# 3. 避免一次性加载
大文件不要使用:
Files.readAllBytes(path);
Files.readAllLines(path);
改为流式处理:
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.forEach(this::process);
}
# 4. 减少不必要的转换
频繁在 byte[]、String、对象之间转换会增加 CPU 和内存压力。
例如网络代理只是转发二进制数据,就不应该转换成字符串。
byte[] -> String -> byte[]
这不仅慢,还可能因为字符集导致数据损坏。
# 5. 使用 FileChannel 和零拷贝
大文件复制或发送可以考虑:
fileChannel.transferTo(position, count, targetChannel);
适合数据搬运,不适合需要逐字节业务处理的场景。
# 6. 使用内存映射
大文件随机访问可以考虑 MappedByteBuffer。
适合:
- 大文件索引。
- 固定格式数据。
- 高频随机读。
要注意:
- 映射释放。
- 直接内存。
- 文件锁和跨平台行为。
- 崩溃一致性。
# 7. 线程模型优化
IO 密集任务可以适当增加线程数,但不是越多越好。
问题包括:
- 线程太少:IO 等待期间 CPU 利用不充分。
- 线程太多:上下文切换和内存占用增加。
- 磁盘或下游已经饱和:增加线程只会排队更严重。
网络高并发场景应考虑 NIO 或成熟框架。
# 8. 批量和合并
频繁小写入可以合并:
- 批量写日志。
- 批量刷盘。
- 批量网络发送。
- 使用缓冲队列削峰。
但批量会增加延迟和丢失窗口,需要在吞吐和可靠性之间权衡。
# 专家实践清单
| 优化方向 | 先看指标 | 可能手段 |
|---|---|---|
| 减少系统调用 | 调用频率、吞吐 | 缓冲、批量 |
| 降低内存压力 | GC、分配速率 | 复用缓冲、流式处理 |
| 减少复制 | CPU、上下文切换 | 零拷贝、mmap |
| 避免阻塞扩散 | 线程状态、队列 | 线程池隔离、超时 |
| 提升可靠性 | 错误率、重试量 | 限流、幂等、临时文件 |
优化顺序:
测量瓶颈
▼
消除明显错误
▼
调整缓冲和批量
▼
隔离线程和下游
▼
评估高级 IO 技术
# Tips 快问快答
Q:IO 优化第一步是什么? A:测量瓶颈,确认慢在磁盘、网络、CPU、内存、锁还是下游。
Q:为什么缓冲能提升性能? A:减少系统调用和小块读写次数。
Q:为什么批量能提升吞吐? A:摊薄固定开销,但会增加延迟和失败窗口。
Q:零拷贝适合所有 IO 吗? A:不适合,只适合数据搬运类场景。
Q:IO 线程池为什么要隔离? A:慢 IO 会占满线程,拖垮其他业务。
Q:大 byte[] 分配有什么风险? A:增加 GC 压力,甚至导致内存碎片或 OOM。
Q:优化后怎么证明有效? A:对比吞吐、P95/P99、CPU、GC、队列长度和错误率。
Q:高级优化最大风险是什么? A:复杂度提升但收益不明显,且故障排查更难。
# 9. 总结
IO 性能优化的顺序应是:测量瓶颈、减少小 IO、避免一次性加载、减少不必要转换、选择合适模型、再考虑零拷贝和内存映射。越高级的技术,越需要明确收益和维护成本。