IO 性能优化
面试提问
"Java IO 性能优化的常见手段有哪些?"
IO 性能瓶颈
| 瓶颈 | 原因 | 优化方向 |
|---|---|---|
| 磁盘 IO | 物理寻道慢 | 顺序读写、缓冲 |
| 网络 IO | 网络延迟 | 批量传输、压缩 |
| 上下文切换 | 系统调用多 | 零拷贝、NIO |
| 内存拷贝 | 数据多次复制 | 直接内存、零拷贝 |
缓冲优化
使用缓冲流
// 不推荐:无缓冲
FileInputStream fis = new FileInputStream("file.txt");
int b;
while ((b = fis.read()) != -1) { // 每次读 1 字节,大量系统调用
// 处理
}
// 推荐:使用缓冲流
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("file.txt"));
int b;
while ((b = bis.read()) != -1) { // 批量读取,减少系统调用
// 处理
}自定义缓冲大小
// 默认缓冲区大小:8KB
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("file.txt"), 64 * 1024); // 64KB 缓冲
// 自定义缓冲读取
byte[] buffer = new byte[64 * 1024];
int len;
while ((len = fis.read(buffer)) != -1) {
// 处理 buffer
}缓冲大小建议
| 场景 | 建议大小 |
|---|---|
| 小文件 | 8KB - 16KB |
| 大文件 | 64KB - 256KB |
| 网络传输 | 4KB - 16KB |
| SSD 磁盘 | 64KB - 1MB |
直接内存(DirectBuffer)
堆内存 vs 直接内存
// 堆内存 Buffer
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 直接内存 Buffer(零拷贝友好)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);性能对比
| 对比项 | 堆内存 | 直接内存 |
|---|---|---|
| 分配速度 | 快 | 慢 |
| IO 性能 | 低(需复制) | 高(零拷贝) |
| GC 影响 | 受 GC 管理 | 不受 GC 直接管理 |
| 适用场景 | 短生命周期 | 长生命周期、频繁 IO |
使用建议
// 频繁 IO 操作使用直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(64 * 1024);
// 短生命周期使用堆内存
ByteBuffer buffer = ByteBuffer.allocate(1024);NIO 多路复用
BIO vs NIO 并发能力
// BIO:每个连接一个线程
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> handle(socket)).start(); // 线程资源有限
}
// NIO:一个线程管理多个连接
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
// 处理就绪的 Channel
}连接数对比
| 模型 | 连接数上限 | 资源消耗 |
|---|---|---|
| BIO | ~1000 | 高(每连接一线程) |
| NIO | 10000+ | 低(线程池 + Selector) |
零拷贝
传统 IO vs 零拷贝
// 传统 IO:4 次拷贝,4 次上下文切换
byte[] data = Files.readAllBytes(Paths.get("file.txt"));
socket.getOutputStream().write(data);
// 零拷贝:2 次 DMA 拷贝,2 次上下文切换
FileChannel fileChannel = new FileInputStream("file.txt").getChannel();
fileChannel.transferTo(0, fileChannel.size(), socketChannel);适用场景
- 文件传输服务
- 消息队列(Kafka、RocketMQ)
- 静态资源服务
批量操作
批量读取
// 不推荐:逐行读取处理
List<String> lines = Files.readAllLines(path); // 一次性加载到内存
for (String line : lines) {
process(line);
}
// 推荐:流式处理
try (Stream<String> stream = Files.lines(path)) {
stream.forEach(this::process); // 逐行处理,内存友好
}批量写入
// 不推荐:多次写入
for (String line : lines) {
writer.write(line); // 多次系统调用
}
// 推荐:批量写入
StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(line).append("\n");
}
writer.write(sb.toString()); // 一次写入异步 IO
异步文件读写
// 异步读取
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Paths.get("file.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future<Integer> future = channel.read(buffer, 0);
// 做其他事情...
Integer bytesRead = future.get(); // 阻塞等待完成
// 回调方式
channel.read(buffer, 0, null, new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
// 读取完成
}
@Override
public void failed(Throwable exc, Void attachment) {
// 处理异常
}
});异步 Socket
AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
serverChannel.accept(null, this); // 继续接受连接
// 处理客户端
}
@Override
public void failed(Throwable exc, Void attachment) {
// 处理异常
}
});文件系统优化
顺序读写 vs 随机读写
// 随机读写:性能差
RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");
raf.seek(100); // 定位
raf.write(data);
raf.seek(500); // 再定位
raf.write(data2);
// 顺序读写:性能好
try (FileChannel channel = new FileOutputStream("file.txt").getChannel()) {
for (byte[] data : dataList) {
channel.write(ByteBuffer.wrap(data)); // 顺序写入
}
}内存映射文件
// 大文件处理
FileChannel channel = new RandomAccessFile("large.txt", "r").getChannel();
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY,
0,
Math.min(channel.size(), Integer.MAX_VALUE));
// 直接从内存读取
while (buffer.hasRemaining()) {
byte b = buffer.get();
// 处理
}网络传输优化
TCP 参数优化
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket socket = serverChannel.socket();
socket.setReuseAddress(true); // 允许端口重用
socket.setReceiveBufferSize(64 * 1024); // 接收缓冲区大小
SocketChannel clientChannel = serverChannel.accept();
Socket clientSocket = clientChannel.socket();
clientSocket.setTcpNoDelay(true); // 禁用 Nagle 算法
clientSocket.setKeepAlive(true); // 保持连接
clientSocket.setSendBufferSize(64 * 1024); // 发送缓冲区大小数据压缩
// 压缩传输
try (GZIPOutputStream gzip = new GZIPOutputStream(
socket.getOutputStream())) {
gzip.write(data);
}
// 解压接收
try (GZIPInputStream gzip = new GZIPInputStream(
socket.getInputStream())) {
byte[] buffer = new byte[1024];
int len;
while ((len = gzip.read(buffer)) != -1) {
// 处理
}
}性能监控
关键指标
| 指标 | 说明 | 工具 |
|---|---|---|
| IOPS | 每秒 IO 次数 | iostat |
| 吞吐量 | 每秒数据量 | iostat |
| 延迟 | IO 响应时间 | iostat |
| 队列深度 | 等待的 IO 请求数 | iostat |
Java 监控
// 文件监控
FileChannel channel = FileChannel.open(path);
long position = channel.position();
long size = channel.size();
// 网络监控
Socket socket = channel.socket();
int sendBuffer = socket.getSendBufferSize();
int receiveBuffer = socket.getReceiveBufferSize();面试要点总结
| 问题 | 答案要点 |
|---|---|
| 如何优化 IO 性能? | 缓冲、NIO、零拷贝、直接内存 |
| 缓冲大小怎么选? | 小文件 8-16KB,大文件 64-256KB |
| 什么时候用直接内存? | 频繁 IO、长生命周期对象 |
| 零拷贝优势? | 减少 CPU 拷贝和上下文切换 |
| 顺序 vs 随机读写? | 顺序读写性能更好 |
参考资料
- 《Java 性能优化权威指南》
- Linux IO 调度 (opens in a new tab)
- Netty 性能优化指南