第一部分:范式转移------从 BIO 到 NIO 的进化
你必须理解为什么要引入 NIO。
1. BIO 的阿克琉斯之踵
传统的 BIO(Blocking IO)是面向流 的,且是阻塞的。
- 痛点:一个连接一个线程。在高并发场景下,线程切换的上下文开销(Context Switch)和内存占用(每个线程默认 1MB 栈空间)会直接拖垮 CPU。
2. NIO 的降维打击
NIO 引入了缓冲区(Buffer)*和* 通道(Channel)**的概念,它是**面向块的。
- 核心优势:非阻塞模型。通过一个线程(Selector)轮询成千上万个连接,只有在真正有读写事件发生时才触发处理。
第二部分:核心三剑客------Buffer、Channel、Selector
1. Buffer(缓冲区):数据的"中转站"
在 NIO 中,所有数据都是通过 Buffer 处理的。Buffer 实质上是一个数组,但它比普通数组高级在有三个关键指针:
-
Capacity:容量,最大存储量。
-
Position:下一个读/写位置。
-
Limit:界限,表示缓冲区中当前有效数据的边界。
面试避坑指南 :写模式切读模式一定要调用
flip()!它会将limit设为当前position,再将position置为 0,这样你读到的才是刚刚写入的数据。
2. Channel(通道):数据的"高速公路"
Channel 是双向的,既可以读也可以写(流是单向的)。
-
FileChannel:用于本地文件 IO。
-
SocketChannel / ServerSocketChannel:用于 TCP 网络通讯。
3. Selector(选择器):多路复用的"指挥官"
这是 NIO 实现单线程管理多连接的关键。它基于操作系统的 epoll(Linux)实现。
第三部分:硬核考点------零拷贝(Zero-Copy)
这是大厂面试的"压轴题"。附件中重点提到了两种实现:
1. mmap (内存映射)
通过 FileChannel.map() 实现。它将文件直接映射到用户态的虚拟地址空间。
-
原理:减少了一次内核空间到用户空间的 CPU 拷贝。
-
Java 类 :
MappedByteBuffer。
2. sendfile
通过 FileChannel.transferTo() 实现。
- 原理:数据直接在内核态从"磁盘缓冲区"拷贝到"网卡缓冲区",甚至不经过 CPU 翻炒。这是 Kafka 实现超高性能吞吐的底层武器。
第四部分:Java 代码实战------高性能文件读写
1. 利用直接内存(Direct Buffer)提升效率
Java
public void fastCopy(String src, String dest) throws IOException {
FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest);
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// 分配直接内存(堆外内存),减少一次内核到堆内的拷贝
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 10);
while (inChannel.read(buffer) != -1) {
buffer.flip(); // 切换为读模式
outChannel.write(buffer);
buffer.clear(); // 清空,切换为写模式
}
}
第五部分:面试复盘脑图
Code snippet
mindmap
root((Java NIO 核心))
基本组件
Buffer: 读写缓冲区 (capacity, position, limit)
Channel: 双向通道 (File, Socket, Datagram)
Selector: 多路复用器 (SelectionKey 事件驱动)
核心特性
非阻塞: 线程无需等待 IO 就绪
面向块: 以 Buffer 为单位, 效率更高
性能优化
直接内存 (DirectBuffer): 规避垃圾回收干扰, 减少拷贝
零拷贝 (Zero-Copy): transferTo() & mmap
内存映射 (MappedByteBuffer): 适合大文件读写
对比 BIO
BIO: 流式、同步阻塞、一连接一线程
NIO: 块式、同步非阻塞、多路复用
应用场景
高性能服务器 (Netty)
大数据传输 (Kafka, RocketMQ)
分布式文件系统
第六部分:大厂面试官的"深度思考题"
-
直接内存(Direct Buffer)既然快,为什么不全用它?
- 回答要点:直接内存的申请和释放开销很大(不受 JVM 直接管理)。它适合生命周期长、频繁 IO 的大缓冲区。小数据量、短生命周期的对象用堆内存(Heap Buffer)配合 GC 更好。
-
Selector 为什么在 Linux 上比 Windows 强?
- 回答要点 :Linux 使用
epoll,它是基于事件回调的,复杂度 O(1);Windows 使用select(或类似的机制),需要线性轮询所有连接,复杂度 O(n)。
- 回答要点 :Linux 使用
-
什么是 Selector 的"空轮询" Bug?如何解决?
- 回答要点:这是早期 JDK 的一个 Bug,导致 Selector 即使没有事件也会被唤醒,CPU 飙升 100%。Netty 的做法是:统计轮询次数,如果短时间内触发多次空轮询,就重建一个新的 Selector 并重新注册所有 Channel。
结语:从"理解概念"到"掌控性能"
我见过太多的系统因为 IO 瓶颈而崩溃。NIO 并不是银弹,它增加了编程的复杂度,但它为 Java 带来了处理海量并发的能力。
如果你能讲清楚 flip() 后的指针变化,能分析出 mmap 的缺页异常,能利用 transferTo 优化数据传输,那么你已经站在了 Java 后端开发的第一梯队。