01.05 Java基础篇|I/O、NIO 与序列化实战
导读
-
适用人群:需要掌握 Java 文件、网络 I/O 与数据序列化的开发者
-
学习目标:深入理解 BIO/NIO/AIO 模型、序列化策略,掌握 I/O 模型在网络编程和并发编程中的应用
-
阅读建议 :
- 先理解阻塞/非阻塞 I/O 的本质区别
- 掌握 NIO 的核心组件(Channel、Buffer、Selector)
- 理解 I/O 模型如何支撑网络编程(参考 01.06 网络编程与协议实践)
- 了解 I/O 模型在并发编程中的应用(参考 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比])
-
知识关联图谱 :
I/O 模型(本文档) ├── 网络编程(01.06) │ ├── Socket 编程(基于 BIO/NIO) │ ├── HTTP 客户端/服务器(基于 NIO) │ ├── WebSocket(基于 NIO) │ └── 协议编解码(基于 ByteBuffer) │ └── 并发编程(02.04) ├── Netty(基于 NIO Selector) ├── Reactor 模式(基于 NIO 事件驱动) ├── WebFlux(基于 Netty + Reactor) └── 响应式编程(基于非阻塞 I/O) -
学习路径 :
- 基础阶段(本文档):掌握 I/O 模型、ByteBuffer、零拷贝、序列化
- 应用阶段(01.06):基于 I/O 模型实现网络协议(TCP/UDP/HTTP/WebSocket)
- 进阶阶段(02.04):基于 NIO 实现高性能并发框架(Netty、Reactor)
核心知识架构
I/O 模型全景对比
阻塞 I/O(BIO - Blocking I/O)
工作原理:
java
// 传统 BIO 服务器
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待连接
new Thread(() -> {
// 每个连接一个线程
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()))) {
String line;
while ((line = reader.readLine()) != null) { // 阻塞读取
writer.write("Echo: " + line + "\n");
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
问题分析:
- 线程开销:每个连接一个线程,线程创建和上下文切换成本高
- 资源限制:线程数量受限于操作系统(通常几千个)
- 内存占用:每个线程需要 1MB 栈空间,10000 连接需要 10GB 内存
- 适用场景:连接数少(< 1000)、业务处理快的场景
与网络编程的关联:
- BIO 是传统 Socket 编程的基础模型
- 在 01.06 网络编程与协议实践中会看到如何基于 BIO 实现 HTTP 服务器
与并发编程的关联:
- BIO 的线程模型是"一连接一线程",这是最基础的并发模型
- 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比]中会看到 Netty 如何基于 NIO 优化这种模型
非阻塞 I/O(NIO - Non-blocking I/O)
核心组件:
- Channel(通道):双向数据传输,替代传统 Stream
- Buffer(缓冲区):数据容器,提供高效的数据操作
- Selector(选择器):多路复用器,单线程管理多个 Channel
工作原理:
java
// NIO 服务器完整示例
public class NioEchoServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 非阻塞模式
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
selector.select(); // 阻塞直到有事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // 重要:必须移除
if (key.isAcceptable()) {
// 处理连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接: " + client.getRemoteAddress());
} else if (key.isReadable()) {
// 处理读
SocketChannel channel = (SocketChannel) key.channel();
buffer.clear();
int read = channel.read(buffer);
if (read == -1) {
channel.close();
continue;
}
buffer.flip();
channel.write(buffer); // Echo 回写
}
}
}
}
}
Selector 底层原理(epoll/kqueue/IOCP):
Linux epoll:
c
// epoll 系统调用(简化)
int epoll_fd = epoll_create1(0);
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 边缘触发
event.data.fd = socket_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event);
// 等待事件
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
// O(1) 复杂度,只返回就绪的文件描述符
性能对比:
- BIO:O(n) 复杂度,需要遍历所有连接检查状态
- NIO(epoll):O(1) 复杂度,只返回就绪的连接
- 优势:单线程可以处理数万个连接
与网络编程的关联:
- NIO 是高性能网络编程的基础,Netty、Tomcat NIO Connector 都基于 NIO
- 在01.06 网络编程与协议实践中会看到如何基于 NIO 实现 HTTP/2、WebSocket 等协议
与并发编程的关联:
- NIO 的事件驱动模型是响应式编程的基础
- Netty 的 EventLoop 基于 NIO Selector 实现
- 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比]中会看到 Reactor 模式如何基于 NIO 实现
异步 I/O(AIO - Asynchronous I/O)
特点:
- 真正的异步:操作系统级别异步,不占用线程
- 回调机制 :通过
CompletionHandler处理结果 - 适用场景:大量长连接、高并发场景
示例:
java
// AIO 服务器
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel channel, Void attachment) {
// 继续接受下一个连接
server.accept(null, this);
// 处理当前连接
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (result > 0) {
buffer.flip();
channel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// 写入完成
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
AIO vs NIO:
- NIO:非阻塞,但需要轮询检查状态
- AIO:异步,操作系统主动通知
- 实际应用:Linux AIO 实现不完善,实际项目中 NIO 更常用
ByteBuffer 状态机深度解析
四个核心属性
java
public abstract class ByteBuffer {
private int mark = -1; // 标记位置(用于 reset)
private int position = 0; // 当前位置(读写指针)
private int limit; // 限制位置(可读/可写的边界)
private int capacity; // 容量(缓冲区大小)
// 关系:mark <= position <= limit <= capacity
}
状态转换图
初始状态: [position=0, limit=capacity, mark=-1]
↓ write/put()
写入状态: [position=写入位置, limit=capacity]
↓ flip()
读取状态: [position=0, limit=原position]
↓ read/get()
读取后: [position=读取位置, limit=原position]
↓ clear() 或 compact()
重置状态
核心方法详解
java
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 1. put() - 写入数据
buffer.put("Hello".getBytes());
// 状态:position=5, limit=1024, capacity=1024
// 2. flip() - 准备读取(将 limit 设为 position,position 设为 0)
buffer.flip();
// 状态:position=0, limit=5, capacity=1024
// 3. get() - 读取数据
byte[] data = new byte[buffer.remaining()]; // remaining() = limit - position
buffer.get(data);
// 状态:position=5, limit=5, capacity=1024
// 4. clear() - 清空,准备写入(position=0, limit=capacity)
buffer.clear();
// 状态:position=0, limit=1024, capacity=1024
// 注意:数据还在,但标记为可覆盖
// 5. compact() - 保留未读数据,准备写入
buffer.put("Hello World".getBytes());
buffer.flip();
buffer.get(new byte[5]); // 读取 "Hello"
buffer.compact(); // " World" 移到开头,position=6, limit=1024
// 6. rewind() - 重置 position,不改变 limit
buffer.rewind(); // position=0
// 7. mark() 和 reset() - 标记和重置
buffer.mark(); // 标记当前位置
buffer.position(10);
buffer.reset(); // 回到标记位置
直接缓冲区 vs 堆缓冲区
java
// 堆缓冲区:在 JVM 堆中
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 优点:GC 管理,分配快
// 缺点:与操作系统交互时需要拷贝
// 直接缓冲区:在操作系统内存中
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
// 优点:减少一次拷贝(JVM 堆 → 操作系统内存)
// 缺点:分配和释放成本高,不受 GC 管理
// 使用建议:
// - 大文件传输:使用 DirectBuffer
// - 小数据操作:使用 HeapBuffer
// - Netty 默认使用 DirectBuffer(通过 PooledByteBufAllocator)
与网络编程的关联:
- ByteBuffer 是网络数据传输的基础容器
- 在 01.06 网络编程与协议实践中会看到如何用 ByteBuffer 实现协议编解码
与并发编程的关联:
- Netty 的 ByteBuf 是对 ByteBuffer 的增强,支持引用计数和池化
- 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比]中会看到 Netty 如何优化缓冲区管理
零拷贝(Zero-copy)原理深度解析
传统 I/O 流程(4 次拷贝)
磁盘文件
↓ (DMA 拷贝)
内核缓冲区
↓ (CPU 拷贝)
用户缓冲区(JVM 堆)
↓ (CPU 拷贝)
Socket 缓冲区
↓ (DMA 拷贝)
网卡
问题:4 次拷贝,2 次 CPU 参与,性能开销大
零拷贝流程(2 次拷贝)
sendfile 系统调用:
c
// Linux sendfile 系统调用
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
// 直接从文件描述符传输到 Socket,无需用户空间参与
磁盘文件
↓ (DMA 拷贝)
内核缓冲区
↓ (DMA 拷贝,CPU 只参与控制)
网卡
优势:
- 减少 2 次拷贝
- 减少 CPU 参与
- 提升吞吐量 2-3 倍
Java 零拷贝实现
1. FileChannel.transferTo():
java
// 使用 sendfile 系统调用
try (FileChannel sourceChannel = new FileInputStream("large.bin").getChannel();
FileChannel destChannel = new FileOutputStream("backup.bin").getChannel()) {
long transferred = sourceChannel.transferTo(0, sourceChannel.size(), destChannel);
System.out.printf("传输了 %d 字节%n", transferred);
}
2. MappedByteBuffer(内存映射):
java
// 将文件映射到内存
try (RandomAccessFile file = new RandomAccessFile("large.bin", "r");
FileChannel channel = file.getChannel()) {
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size()
);
// 直接操作内存,操作系统负责同步到磁盘
byte[] data = new byte[(int) channel.size()];
buffer.get(data);
}
3. Netty 的零拷贝:
java
// Netty 使用 DirectBuffer 和 CompositeByteBuf 实现零拷贝
// 在 [01.02.04 Java并发编程|Stream、响应式与 Netty 全景对比.md](01.02.04%20Java并发编程|Stream、响应式与%20Netty%20全景对比.md) 中详细讲解
与网络编程的关联:
- 零拷贝是高性能网络传输的关键技术
- HTTP 文件下载、FTP 传输都使用零拷贝优化
- 在 01.06 网络编程与协议实践中会看到零拷贝在网络协议中的应用
序列化策略深度解析
JDK 默认序列化
基本用法:
java
// 实现 Serializable 接口
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 版本号
private String name;
private int age;
// transient 字段不序列化
private transient String password;
}
// 序列化
User user = new User("Alice", 25);
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.ser"));
oos.writeObject(user);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.ser"));
User deserialized = (User) ois.readObject();
ois.close();
问题:
- 性能低:反射调用,序列化/反序列化慢
- 体积大:包含完整类信息,比 JSON 大 2-10 倍
- 安全问题:反序列化漏洞(如 Apache Commons Collections)
- 版本兼容:字段变化可能导致反序列化失败
安全防护:
java
// 白名单机制
ObjectInputStream ois = new ObjectInputStream(input) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String className = desc.getName();
if (!isAllowed(className)) {
throw new ClassNotFoundException("Not allowed: " + className);
}
return super.resolveClass(desc);
}
private boolean isAllowed(String className) {
return className.startsWith("com.example.") &&
!className.contains("$"); // 禁止内部类
}
};
JSON 序列化
Jackson 示例:
java
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 序列化
User user = new User("Alice", 25);
String json = mapper.writeValueAsString(user);
// {"name":"Alice","age":25}
// 反序列化
User deserialized = mapper.readValue(json, User.class);
// 泛型支持
List<User> users = mapper.readValue(jsonArray,
new TypeReference<List<User>>() {});
优势:
- 跨语言兼容
- 可读性好,易调试
- 版本兼容性好(字段可选)
劣势:
- 性能低于二进制协议
- 体积大于二进制协议
二进制序列化
Protobuf 示例:
protobuf
// user.proto
syntax = "proto3";
message User {
string name = 1;
int32 age = 2;
}
java
// 生成 Java 类后使用
UserProto.User user = UserProto.User.newBuilder()
.setName("Alice")
.setAge(25)
.build();
// 序列化
byte[] data = user.toByteArray();
// 反序列化
UserProto.User deserialized = UserProto.User.parseFrom(data);
优势:
- 性能高:比 JSON 快 5-10 倍
- 体积小:比 JSON 小 2-5 倍
- 类型安全:强类型,编译期检查
- 版本兼容:支持字段添加、删除
其他二进制协议:
- Kryo:Java 专用,性能极高
- Avro:支持 Schema 演进
- Hessian:跨语言,体积小
序列化方案选型
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 跨语言通信 | JSON/Protobuf | 语言无关 |
| 高性能 | Kryo/Protobuf | 二进制,速度快 |
| Schema 演进 | Avro/Protobuf | 支持版本兼容 |
| 简单场景 | JSON | 易调试,可读 |
| 内部服务 | Kryo | Java 专用,性能最高 |
与网络编程的关联:
- 序列化是网络通信的基础,HTTP、gRPC、Dubbo 都依赖序列化
- 在01.06 网络编程与协议实践中会看到序列化在网络协议中的应用
与并发编程的关联:
- 序列化性能影响并发吞吐量
- Netty 的编解码器负责序列化/反序列化
- 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比]中会看到 Netty 如何优化序列化性能
实战案例
案例 1:基于 NIO 的文件服务器
需求:实现一个高性能文件服务器,支持多客户端并发下载
java
public class NioFileServer {
private static final int PORT = 8080;
private static final String BASE_DIR = "/data/files";
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("文件服务器启动,端口: " + PORT);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
handleAccept(selector, key);
} else if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
}
}
}
}
private static void handleAccept(Selector selector, SelectionKey key)
throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ, new ClientContext());
}
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ClientContext context = (ClientContext) key.attachment();
ByteBuffer buffer = context.getReadBuffer();
int read = channel.read(buffer);
if (read == -1) {
channel.close();
return;
}
if (read > 0) {
buffer.flip();
String request = StandardCharsets.UTF_8.decode(buffer).toString();
String fileName = parseFileName(request);
// 准备文件传输
Path filePath = Paths.get(BASE_DIR, fileName);
if (Files.exists(filePath)) {
FileChannel fileChannel = FileInputStream(
filePath.toFile()).getChannel();
context.setFileChannel(fileChannel);
context.setFileSize(Files.size(filePath));
key.interestOps(SelectionKey.OP_WRITE);
} else {
channel.write(ByteBuffer.wrap("404 Not Found\n".getBytes()));
channel.close();
}
}
}
private static void handleWrite(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ClientContext context = (ClientContext) key.attachment();
FileChannel fileChannel = context.getFileChannel();
if (fileChannel != null) {
// 使用零拷贝传输
long transferred = fileChannel.transferTo(
context.getPosition(),
context.getFileSize() - context.getPosition(),
channel
);
context.addPosition(transferred);
if (context.getPosition() >= context.getFileSize()) {
fileChannel.close();
channel.close();
}
}
}
private static String parseFileName(String request) {
// 解析 HTTP 请求,提取文件名
String[] lines = request.split("\n");
if (lines.length > 0) {
String[] parts = lines[0].split(" ");
if (parts.length > 1) {
return parts[1].substring(1); // 去掉前导 "/"
}
}
return "index.html";
}
static class ClientContext {
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private FileChannel fileChannel;
private long fileSize;
private long position;
// getters and setters
}
}
技术要点:
- NIO 多路复用:单线程处理多个客户端
- 零拷贝传输 :使用
FileChannel.transferTo()实现高效文件传输 - 状态管理 :通过
SelectionKey.attachment()保存客户端上下文
与网络编程的关联:
- 这是网络编程的基础实现,可以扩展为完整的 HTTP 服务器
- 在 01.06 网络编程与协议实践中会看到如何在此基础上实现 HTTP 协议
案例 2:基于 NIO 的日志聚合系统
需求:实时收集多个应用的日志,聚合后写入文件
java
public class LogAggregator {
private final Selector selector;
private final Path logFile;
private final FileChannel fileChannel;
private final ByteBuffer writeBuffer;
public LogAggregator(int port, Path logFile) throws IOException {
this.selector = Selector.open();
this.logFile = logFile;
this.fileChannel = FileChannel.open(
logFile,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.APPEND
);
this.writeBuffer = ByteBuffer.allocateDirect(8192);
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
server.register(selector, SelectionKey.OP_ACCEPT);
}
public void start() throws IOException {
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
acceptConnection(key);
} else if (key.isReadable()) {
readLog(key);
}
}
}
}
private void acceptConnection(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ,
ByteBuffer.allocateDirect(4096));
}
private void readLog(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int read = channel.read(buffer);
if (read == -1) {
channel.close();
return;
}
if (read > 0) {
buffer.flip();
// 写入文件(批量写入,提升性能)
while (buffer.hasRemaining()) {
writeBuffer.clear();
int toWrite = Math.min(buffer.remaining(), writeBuffer.capacity());
buffer.limit(buffer.position() + toWrite);
writeBuffer.put(buffer);
writeBuffer.flip();
fileChannel.write(writeBuffer);
}
buffer.compact();
}
}
}
技术要点:
- 批量写入:使用缓冲区批量写入文件,减少系统调用
- 直接缓冲区:使用 DirectBuffer 提升性能
- 非阻塞处理:单线程处理多个日志源
高频面试问答(深度解析)
1. BIO、NIO、AIO 的区别和适用场景?
标准答案:
| 特性 | BIO | NIO | AIO |
|---|---|---|---|
| 阻塞方式 | 阻塞 I/O | 非阻塞 I/O | 异步 I/O |
| 线程模型 | 一连接一线程 | 单线程多连接 | 异步回调 |
| 复杂度 | 低 | 中 | 高 |
| 适用场景 | 连接数少(< 1000) | 连接数多(> 10000) | 大量长连接 |
| 性能 | 低 | 高 | 最高(理论) |
深入追问与回答思路:
Q: NIO 的"非阻塞"是什么意思?
- BIO :
socket.read()会阻塞直到有数据可读 - NIO :
channel.read(buffer)立即返回,可能返回 0(无数据),需要轮询检查 - AIO:操作系统异步处理,完成后通过回调通知
Q: Selector.select() 是阻塞的吗?
java
// 阻塞:直到有事件或超时
selector.select(); // 无限阻塞
selector.select(1000); // 阻塞最多 1 秒
// 非阻塞:立即返回
selector.selectNow(); // 不阻塞,立即返回
Q: 为什么 NIO 比 BIO 性能好?
- 减少线程开销:单线程处理多连接,从 O(n) 降到 O(1)
- 事件驱动:只处理就绪的连接,避免无效轮询
- 零拷贝:减少数据拷贝次数
Q: 实际项目中如何选择?
- BIO:简单场景,连接数少,如传统 Web 应用
- NIO:高并发场景,如 Netty、Tomcat NIO Connector
- AIO:Linux 实现不完善,实际项目中很少使用
关联知识点:
- 在01.06 网络编程与协议实践中会看到如何基于这些 I/O 模型实现网络协议
- 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比]中会看到 Netty 如何基于 NIO 实现高性能并发
2. ByteBuffer 的常见错误和最佳实践?
标准答案:
- 忘记 flip():写入后直接读取,position 在末尾
- clear() vs compact():clear 清空所有,compact 保留未读数据
- 直接缓冲区未释放:可能导致内存泄漏
- 线程安全:ByteBuffer 不是线程安全的
深入追问与回答思路:
Q: flip() 的作用是什么?
java
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello".getBytes());
// 此时:position=5, limit=1024
buffer.flip();
// 此时:position=0, limit=5
// 准备读取,limit 设置为写入的位置
Q: clear() 和 compact() 的区别?
java
// clear(): 清空所有,position=0, limit=capacity
buffer.clear(); // 数据还在,但标记为可覆盖
// compact(): 保留未读数据
buffer.put("Hello World".getBytes());
buffer.flip();
buffer.get(new byte[5]); // 读取 "Hello"
buffer.compact(); // " World" 移到开头,position=6
Q: 什么时候用 DirectBuffer,什么时候用 HeapBuffer?
- DirectBuffer:大文件传输、网络 I/O、需要零拷贝的场景
- HeapBuffer:小数据操作、频繁分配释放的场景
Q: 如何避免 ByteBuffer 的内存泄漏?
java
// 1. 使用 try-with-resources(如果实现了 AutoCloseable)
// 2. 及时释放直接缓冲区
// 3. 使用 Netty 的 ByteBuf(支持引用计数和自动释放)
关联知识点:
- 在 01.06 网络编程与协议实践中会看到 ByteBuffer 在网络协议编解码中的应用
- 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比.md]中会看到 Netty 的 ByteBuf 如何优化 ByteBuffer
3. 零拷贝的原理和实现方式?
标准答案:
- 传统 I/O:数据从磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡(4 次拷贝)
- 零拷贝:数据从磁盘 → 内核缓冲区 → 网卡(2 次拷贝,减少用户空间拷贝)
深入追问与回答思路:
Q: sendfile 系统调用的原理?
c
// Linux sendfile 系统调用
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
// 直接从文件描述符传输到 Socket,无需用户空间参与
// 减少 2 次拷贝,提升性能 2-3 倍
Q: Java 中的零拷贝实现?
java
// 1. FileChannel.transferTo() - 使用 sendfile
FileChannel source = new FileInputStream("source.txt").getChannel();
FileChannel dest = new FileOutputStream("dest.txt").getChannel();
source.transferTo(0, source.size(), dest);
// 2. MappedByteBuffer - 内存映射
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size()
);
// 3. Netty 的零拷贝 - DirectBuffer + CompositeByteBuf
Q: 零拷贝的性能提升?
- 吞吐量:提升 2-3 倍
- CPU 使用率:降低 50%
- 内存占用:减少用户空间缓冲区
关联知识点:
- 在01.06 网络编程与协议实践中会看到零拷贝在网络文件传输中的应用
- 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比]中会看到 Netty 如何实现零拷贝
4. 如何选择序列化方案?
标准答案:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 跨语言通信 | JSON/Protobuf | 语言无关 |
| 高性能 | Kryo/Protobuf | 二进制,速度快 |
| Schema 演进 | Avro/Protobuf | 支持版本兼容 |
| 简单场景 | JSON | 易调试,可读 |
| 内部服务 | Kryo | Java 专用,性能最高 |
深入追问与回答思路:
Q: Protobuf 的优势?
- 性能:二进制格式,体积小,速度快(比 JSON 快 5-10 倍)
- 类型安全:强类型,编译期检查
- 版本兼容:支持字段添加、删除
- 跨语言:支持多种语言
Q: JSON vs 二进制序列化?
java
// JSON: 可读性好,但体积大
{"id":1,"name":"Alice"} // 25 字节
// Protobuf: 体积小,但不可读
[0x08, 0x01, 0x12, 0x05, 0x41, 0x6c, 0x69, 0x63, 0x65] // 9 字节
Q: 如何防止反序列化漏洞?
- 白名单机制:只允许反序列化指定的类
- 使用安全的序列化库:如 Protobuf、Kryo
- 避免反序列化不可信数据
关联知识点:
- 在 01.06 网络编程与协议实践中会看到序列化在网络协议中的应用
- 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比] 中会看到 Netty 的编解码器如何优化序列化性能
延伸阅读
- 官方文档:Java I/O Tutorial、NIO.2(JDK 7)指南
- 经典书籍:《Netty in Action》、《Java 并发编程实战》I/O 章节
- 开源示例:Netty、Tomcat NIO Connector、Kafka 对 NIO 的使用
- 工具链 :
async-profiler、jfr分析 I/O 与序列化性能瓶颈 - 关联文档 :
- 01.06 网络编程与协议实践- I/O 模型在网络编程中的应用
-
02.04 Java并发编程|Stream、响应式与 Netty 全景对比\]- I/O 模型在并发编程中的应用
学习路径建议:
- 先掌握 I/O 模型(BIO/NIO/AIO)和 ByteBuffer
- 学习网络编程(Socket、HTTP、WebSocket),理解 I/O 模型在网络编程中的应用
- 学习并发编程(Netty、Reactor),理解 I/O 模型如何支撑高性能并发