Java NIO核心组件详解
约 1899 字大约 6 分钟
javanioio
2025-03-14
概述
Java NIO(New I/O)是 JDK 1.4 引入的一套新的I/O API,提供了面向缓冲区(Buffer-oriented)、基于通道(Channel-based)的I/O操作方式。与传统BIO(Blocking I/O)最大的区别是NIO支持非阻塞模式和I/O多路复用(Selector),适合构建高并发网络应用。
NIO vs BIO
| 特性 | BIO | NIO |
|---|---|---|
| I/O模型 | 阻塞同步 | 非阻塞同步 |
| 数据操作单位 | 流(Stream) | 缓冲区(Buffer) |
| 线程模型 | 一连接一线程 | 一线程管理多连接 |
| 选择器 | 无 | Selector |
| 适用场景 | 连接少、数据量大 | 连接多、数据量小 |
三大核心组件
Buffer(缓冲区)
Buffer 是一块可以读写数据的内存区域。NIO中所有数据的读写都通过Buffer进行。
核心属性
// Buffer的四个核心属性(不等式关系)
// 0 <= mark <= position <= limit <= capacity
public abstract class Buffer {
private int mark = -1; // 标记位置,用于reset
private int position; // 当前读写位置
private int limit; // 有效数据边界
private int capacity; // 总容量,创建后不可变
}关键操作
// 创建Buffer
ByteBuffer buf = ByteBuffer.allocate(1024); // 堆内存
ByteBuffer direct = ByteBuffer.allocateDirect(1024); // 直接内存(堆外)
// 写入数据
buf.put((byte) 65); // 写入一个字节
buf.putInt(42); // 写入一个int(4字节)
buf.put(new byte[]{1, 2, 3}); // 批量写入
// 切换为读模式
buf.flip();
// position → 0, limit → 原position
// 读取数据
byte b = buf.get(); // 读取一个字节
int val = buf.getInt(); // 读取一个int
// 清空Buffer(准备重新写入)
buf.clear(); // position → 0, limit → capacity(数据未清除)
buf.compact(); // 将未读数据移到起始位置,从其后继续写
// 标记与重置
buf.mark(); // mark = position
buf.reset(); // position = mark(回到标记位置重新读取)
// 倒带(重新读取所有数据)
buf.rewind(); // position → 0, mark → -1Buffer状态转换
ByteBuffer的特殊方法
// 包装已有数组
byte[] data = new byte[]{1, 2, 3, 4, 5};
ByteBuffer wrapped = ByteBuffer.wrap(data); // 共享同一底层数组
// 切片(共享底层数据)
buf.position(2);
buf.limit(5);
ByteBuffer slice = buf.slice(); // 新Buffer,范围[2, 5)
// 只读视图
ByteBuffer readOnly = buf.asReadOnlyBuffer();
// 字节序
buf.order(ByteOrder.BIG_ENDIAN); // 默认
buf.order(ByteOrder.LITTLE_ENDIAN); // 小端序
// 视图Buffer
IntBuffer intBuf = buf.asIntBuffer(); // 按int视图操作
CharBuffer charBuf = buf.asCharBuffer(); // 按char视图操作Channel(通道)
Channel 类似于流(Stream),但它是双向的(可读可写),且可以非阻塞操作。
FileChannel
// 读取文件
try (FileChannel fc = FileChannel.open(Path.of("data.txt"), StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
while (fc.read(buf) != -1) {
buf.flip();
// 处理数据...
buf.clear();
}
}
// 写入文件
try (FileChannel fc = FileChannel.open(Path.of("output.txt"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
ByteBuffer buf = ByteBuffer.wrap("Hello NIO".getBytes());
fc.write(buf);
}
// 零拷贝传输
try (FileChannel src = FileChannel.open(Path.of("source.dat"), StandardOpenOption.READ);
FileChannel dst = FileChannel.open(Path.of("dest.dat"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
src.transferTo(0, src.size(), dst); // 操作系统级零拷贝
}Scatter / Gather
// Scatter Read(分散读取)— 将一个Channel的数据读到多个Buffer
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {header, body};
channel.read(buffers); // 先填满header,再填body
// Gather Write(聚集写入)— 将多个Buffer的数据写到一个Channel
channel.write(buffers); // 依次写出header和body的有效数据内存映射文件(Memory-Mapped Files)
// 将文件映射到内存,直接操作内存即操作文件
try (FileChannel fc = FileChannel.open(Path.of("large.dat"), StandardOpenOption.READ)) {
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
// mbb可以像普通ByteBuffer一样读取
// 底层由操作系统的虚拟内存机制管理
while (mbb.hasRemaining()) {
byte b = mbb.get();
}
}内存映射适合处理大文件,操作系统会按需加载页面到内存。
Selector(选择器)
Selector 是NIO的核心,实现 I/O多路复用 — 一个线程通过一个Selector监听多个Channel的事件。
事件类型
| SelectionKey常量 | 事件 | 适用Channel |
|---|---|---|
OP_ACCEPT | 新连接可接受 | ServerSocketChannel |
OP_CONNECT | 连接建立完成 | SocketChannel |
OP_READ | 可读 | SocketChannel |
OP_WRITE | 可写 | SocketChannel |
NIO服务端示例
public class NioServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞模式
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8080");
while (true) {
selector.select(); // 阻塞,直到有事件就绪
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // 必须手动移除
if (key.isAcceptable()) {
// 新连接
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("New client: " + client.getRemoteAddress());
}
else if (key.isReadable()) {
// 数据可读
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(256);
int bytesRead = client.read(buf);
if (bytesRead == -1) {
key.cancel();
client.close();
} else {
buf.flip();
byte[] data = new byte[buf.remaining()];
buf.get(data);
System.out.println("Received: " + new String(data));
// Echo back
client.write(ByteBuffer.wrap(data));
}
}
}
}
}
}Selector工作流程
直接内存(Direct Buffer)
// 堆内Buffer
ByteBuffer heapBuf = ByteBuffer.allocate(1024);
// 底层:byte[] 在JVM堆中
// 直接内存Buffer
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024);
// 底层:操作系统内存(堆外),通过Unsafe分配| 特性 | 堆内Buffer | 直接内存Buffer |
|---|---|---|
| 分配速度 | 快 | 慢(系统调用) |
| 回收方式 | GC | GC时触发Cleaner |
| I/O效率 | 需要一次复制 | 零拷贝(DMA直接访问) |
| 适用场景 | 短生命周期、频繁分配 | 长生命周期、大块I/O |
常见FAQ
Q: NIO一定比BIO快吗?
A: 不一定。BIO在连接数少、数据量大的场景下(如文件传输)表现也很好。NIO的优势在于高并发连接(如WebSocket、IM),一个线程管理成千上万连接。
Q: 为什么Netty封装了NIO?
A: 原生NIO API使用复杂、有已知bug(如Linux epoll空轮询),且缺少半包/粘包处理、心跳、重连等应用层功能。Netty在NIO之上提供了更高层的抽象和更可靠的实现。
Q: FileChannel可以非阻塞吗?
A: 不能。FileChannel 不支持 configureBlocking(false),文件I/O始终是阻塞的。非阻塞文件I/O需要使用 AsynchronousFileChannel(NIO.2, JDK 7+)。
Q: 直接内存怎么回收?
A: DirectByteBuffer 使用 Cleaner(虚引用机制),当 DirectByteBuffer 被GC回收时,Cleaner 会释放对应的堆外内存。也可以通过 ((DirectBuffer) buf).cleaner().clean() 手动释放。通过 -XX:MaxDirectMemorySize 限制直接内存上限。
总结
Java NIO 通过 Buffer、Channel、Selector 三大组件提供了高效的I/O编程模型。Buffer 负责数据存储,Channel 负责数据传输,Selector 实现I/O多路复用。NIO是构建高并发网络应用的基础,也是 Netty、Tomcat NIO 等框架的底层支撑。理解NIO的工作原理,有助于在实际开发中合理选择I/O模型和框架。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于