IO模型
Java IO 是 Java 程序进行数据输入输出的机制,涵盖了文件、网络等多种数据交互方式。IO 模型从 BIO 到 NIO 再到 AIO 的演进,本质上是为了解决高并发场景下线程资源浪费的问题。
IO 模型分类
操作系统提供了多种 IO 模型,Java 对其进行了封装。
UNIX 五种 IO 模型
- 同步阻塞 I/O
- 同步非阻塞 I/O
- I/O 多路复用
- 信号驱动 I/O
- 异步 I/O
Java 三种 IO 模型
| 模型 | 说明 |
|---|---|
| BIO | 同步阻塞,传统阻塞式 |
| NIO | 同步非阻塞,基于多路复用 |
| AIO | 异步非阻塞,NIO 2 |
内核空间与用户空间
为了保证系统稳定性和安全性,进程地址空间划分为:
| 空间 | 说明 |
|---|---|
| 用户空间 | 应用程序运行空间,不能直接访问内核 |
| 内核空间 | 操作系统运行空间,负责资源管理 |
应用程序无法直接进行 IO 操作,必须通过系统调用请求内核帮忙完成。IO 调用需经历两个步骤:内核等待 IO 设备准备好数据,然后将数据从内核空间拷贝到用户空间。
BIO(Blocking IO)
同步阻塞 IO 模型。应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
特点:一请求一线程,线程阻塞等待。客户端连接数量不高时可行,面对高并发时无能为力。
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
while (in.read(buffer) != -1) {
// 处理数据
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}NIO(Non-blocking IO)
Java NIO 于 JDK 1.4 引入,对应 java.nio 包,本质是 IO 多路复用模型。
IO 多路复用原理
| 步骤 | 说明 |
|---|---|
| 1. 调用 select | 线程询问内核数据是否准备就绪(阻塞) |
| 2. 内核准备好数据 | 返回可读事件 |
| 3. 调用 read | 线程发起 read 调用,数据拷贝(阻塞) |
优势:减少无效系统调用,降低 CPU 资源消耗。单线程通过 Selector 管理多个连接,避免了 BIO 每连接一线程的开销。
三大核心组件
| 组件 | 说明 |
|---|---|
| Buffer | 缓冲区,用于读写数据 |
| Channel | 通道,类似流但可双向读写 |
| Selector | 选择器,实现 IO 多路复用 |
Buffer(缓冲区)
四大核心变量:
| 变量 | 说明 |
|---|---|
capacity | 缓冲区容量,创建后不变 |
position | 当前位置,读写模式会变化 |
limit | 限制位置,写模式下=capacity,读模式下=已写入数据量 |
mark | 标记位置,可通过 reset() 恢复 |
状态切换方法:
| 方法 | 说明 |
|---|---|
flip() | 切换读写:limit=position, position=0 |
clear() | 清空缓冲区:position=0, limit=capacity |
compact() | 压缩已读数据,保留未读数据到开头 |
rewind() | 重置 position=0,可重新读 |
创建方式:
// 非直接缓冲区(堆内存)
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 直接缓冲区(堆外内存,IO 性能更高)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);Channel(通道)
Channel 是双向通道,既可以读也可以写。
| 类型 | 说明 |
|---|---|
| FileChannel | 文件 IO |
| SocketChannel | TCP 客户端 |
| ServerSocketChannel | TCP 服务端 |
| DatagramChannel | UDP |
FileChannel 文件复制:
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(target)) {
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(4096);
while (inChannel.read(buffer) != -1) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
}Selector(选择器)
SelectionKey 四种事件:
| 事件 | 说明 |
|---|---|
OP_READ | 读就绪 |
OP_WRITE | 写就绪 |
OP_CONNECT | 连接就绪(客户端) |
OP_ACCEPT | 接收就绪(服务端) |
Selector 使用流程:
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while (selector.select() > 0) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey sk = it.next();
if (sk.isReadable()) { /* 处理读事件 */ }
if (sk.isAcceptable()) { /* 处理连接事件 */ }
it.remove();
}
}AIO(Asynchronous IO)
JDK 1.7 引入,异步非阻塞 IO。应用程序发起 IO 操作后,操作系统完成 IO 后回调通知应用程序。
- Future:异步返回结果,适合单次调用
- CompletionHandler:回调机制
AIO 还不够广泛,Netty 曾尝试使用 AIO 但后来放弃,Linux 上性能提升不明显。
BIO vs NIO vs AIO
| 特性 | BIO(同步阻塞) | NIO(同步非阻塞) | AIO(异步非阻塞) |
|---|---|---|---|
| 阻塞 | 阻塞 | 非阻塞 | 完全异步 |
| 线程 | 每连接一线程 | 单线程管理多连接 | 操作系统回调 |
| 复杂度 | 简单 | 中等 | 复杂 |
| 适用场景 | 低并发、连接固定 | 高并发、短连接 | 高并发、长连接 |
| JDK 版本 | 1.0 | 1.4 | 1.7 |
选择建议:
- 低并发、短连接 -> BIO(代码简单)
- 高并发、短连接 -> NIO(性能好)
- 高并发、长连接 -> AIO(更高效)
流分类
- 按方向:输入流(Input)、输出流(Output)
- 按类型:字节流、字符流
字符流按 16 位传输,字节流按 8 位传输。
常见 IO 类
| 类型 | 字节流 | 字符流 |
|---|---|---|
| 文件流 | FileInputStream/FileOutputStream | FileReader/FileWriter |
| 缓冲流 | BufferedInputStream/BufferedOutputStream | BufferedReader/BufferedWriter |
| 对象流 | ObjectInputStream/ObjectOutputStream | - |
直接缓冲区 vs 非直接缓冲区
| 特性 | 直接缓冲区 | 非直接缓冲区 |
|---|---|---|
| 内存位置 | 堆外内存 | 堆内存 |
| 创建代价 | 高(需调用系统 IO) | 低(普通对象分配) |
| IO 性能 | 高(减少一次内存复制) | 一般 |
| 适用场景 | 长期存在、大数据量 IO | 短期、频繁创建销毁 |
内存映射文件:
MappedByteBuffer mappedBuffer =
new RandomAccessFile(file, "rw")
.getChannel()
.map(FileChannel.MapMode.READ_WRITE, 0, file.length());NIO 零拷贝
零拷贝是提升 IO 操作性能的重要手段,ActiveMQ、Kafka、RocketMQ、Netty 等都用到零拷贝。
零拷贝原理
零拷贝技术对比:
| 方案 | CPU 拷贝 | DMA 拷贝 | 上下文切换 |
|---|---|---|---|
| 传统 | 2 | 2 | 4 |
| mmap+write | 1 | 2 | 4 |
| sendfile | 1 | 2 | 2 |
| sendfile+DMA gather | 0 | 2 | 2 |
Java 零拷贝支持
| 技术 | 说明 |
|---|---|
MappedByteBuffer | 基于 mmap,直接操作内存 |
FileChannel.transferTo() | 基于 sendfile |
mmap 示例:
FileChannel fc = new RandomAccessFile(file, "r").getChannel();
MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());transferTo 示例:
FileChannel inChannel = new FileInputStream(source).getChannel();
FileChannel outChannel = new FileOutputStream(target).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);零拷贝适用于文件传输(Kafka 消息队列)、大文件读取、高性能网络服务。Netty 默认使用 epoll + 零拷贝提升性能。
Socket 网络编程
TCP 编程
服务端:
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket client = server.accept();
new Thread(() -> handle(client)).start();
}客户端:
Socket client = new Socket("localhost", 8080);
PrintWriter out = new PrintWriter(client.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
out.println("Hello");
System.out.println(in.readLine());UDP 编程
// 服务端
DatagramSocket server = new DatagramSocket(8080);
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
server.receive(packet);
// 客户端
DatagramSocket client = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(
msg.getBytes(), msg.length,
new InetSocketAddress("localhost", 8080));
client.send(packet);IO 最佳实践
| 实践 | 说明 |
|---|---|
| 使用缓冲流 | BufferedInputStream/BufferedReader,减少系统调用 |
| 使用 NIO | 高并发场景使用 NIO 多路复用 |
| 使用 try-with-resource | 自动关闭流,避免资源泄漏 |
| 使用直接缓冲区 | 大文件 IO 使用直接缓冲区提升性能 |
常见问题处理
Selector 空轮询(CPU 100%)
- 这是 JDK 的 epoll bug,Selector.select() 不阻塞直接返回空结果
- JDK 1.7 未根本解决,Netty 通过重建 Selector 规避此问题
- 生产环境建议使用 Netty 而非原生 NIO
直接内存 OutOfMemoryError
- 直接缓冲区(
allocateDirect)使用堆外内存,不受 GC 直接管理 - 使用
Unsafe.freeMemory()或Cleaner释放,Netty 的 ByteBuf 引用计数自动管理 - 限制直接内存总量:
-XX:MaxDirectMemorySize=256m