01.05 Java基础篇|I/O、NIO 与序列化实战

01.05 Java基础篇|I/O、NIO 与序列化实战

导读

  • 适用人群:需要掌握 Java 文件、网络 I/O 与数据序列化的开发者

  • 学习目标:深入理解 BIO/NIO/AIO 模型、序列化策略,掌握 I/O 模型在网络编程和并发编程中的应用

  • 阅读建议

    1. 先理解阻塞/非阻塞 I/O 的本质区别
    2. 掌握 NIO 的核心组件(Channel、Buffer、Selector)
    3. 理解 I/O 模型如何支撑网络编程(参考 01.06 网络编程与协议实践)
    4. 了解 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)
  • 学习路径

    1. 基础阶段(本文档):掌握 I/O 模型、ByteBuffer、零拷贝、序列化
    2. 应用阶段(01.06):基于 I/O 模型实现网络协议(TCP/UDP/HTTP/WebSocket)
    3. 进阶阶段(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 的线程模型是"一连接一线程",这是最基础的并发模型
  • 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比]中会看到 Netty 如何基于 NIO 优化这种模型
非阻塞 I/O(NIO - Non-blocking I/O)

核心组件

  1. Channel(通道):双向数据传输,替代传统 Stream
  2. Buffer(缓冲区):数据容器,提供高效的数据操作
  3. 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)

与网络编程的关联

与并发编程的关联

  • 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();

问题

  1. 性能低:反射调用,序列化/反序列化慢
  2. 体积大:包含完整类信息,比 JSON 大 2-10 倍
  3. 安全问题:反序列化漏洞(如 Apache Commons Collections)
  4. 版本兼容:字段变化可能导致反序列化失败

安全防护

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 专用,性能最高

与网络编程的关联

与并发编程的关联

  • 序列化性能影响并发吞吐量
  • 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
    }
}

技术要点

  1. NIO 多路复用:单线程处理多个客户端
  2. 零拷贝传输 :使用 FileChannel.transferTo() 实现高效文件传输
  3. 状态管理 :通过 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();
        }
    }
}

技术要点

  1. 批量写入:使用缓冲区批量写入文件,减少系统调用
  2. 直接缓冲区:使用 DirectBuffer 提升性能
  3. 非阻塞处理:单线程处理多个日志源

高频面试问答(深度解析)

1. BIO、NIO、AIO 的区别和适用场景?

标准答案

特性 BIO NIO AIO
阻塞方式 阻塞 I/O 非阻塞 I/O 异步 I/O
线程模型 一连接一线程 单线程多连接 异步回调
复杂度
适用场景 连接数少(< 1000) 连接数多(> 10000) 大量长连接
性能 最高(理论)

深入追问与回答思路

Q: NIO 的"非阻塞"是什么意思?

  • BIOsocket.read() 会阻塞直到有数据可读
  • NIOchannel.read(buffer) 立即返回,可能返回 0(无数据),需要轮询检查
  • AIO:操作系统异步处理,完成后通过回调通知

Q: Selector.select() 是阻塞的吗?

java 复制代码
// 阻塞:直到有事件或超时
selector.select();        // 无限阻塞
selector.select(1000);    // 阻塞最多 1 秒

// 非阻塞:立即返回
selector.selectNow();     // 不阻塞,立即返回

Q: 为什么 NIO 比 BIO 性能好?

  1. 减少线程开销:单线程处理多连接,从 O(n) 降到 O(1)
  2. 事件驱动:只处理就绪的连接,避免无效轮询
  3. 零拷贝:减少数据拷贝次数

Q: 实际项目中如何选择?

  • BIO:简单场景,连接数少,如传统 Web 应用
  • NIO:高并发场景,如 Netty、Tomcat NIO Connector
  • AIO:Linux 实现不完善,实际项目中很少使用

关联知识点

  • 01.06 网络编程与协议实践中会看到如何基于这些 I/O 模型实现网络协议
  • 在 [02.04 Java并发编程|Stream、响应式与 Netty 全景对比]中会看到 Netty 如何基于 NIO 实现高性能并发

2. ByteBuffer 的常见错误和最佳实践?

标准答案

  1. 忘记 flip():写入后直接读取,position 在末尾
  2. clear() vs compact():clear 清空所有,compact 保留未读数据
  3. 直接缓冲区未释放:可能导致内存泄漏
  4. 线程安全: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(支持引用计数和自动释放)

关联知识点


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: 如何防止反序列化漏洞?

  1. 白名单机制:只允许反序列化指定的类
  2. 使用安全的序列化库:如 Protobuf、Kryo
  3. 避免反序列化不可信数据

关联知识点

  • 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-profilerjfr 分析 I/O 与序列化性能瓶颈
  • 关联文档
    • 01.06 网络编程与协议实践- I/O 模型在网络编程中的应用
    • 02.04 Java并发编程|Stream、响应式与 Netty 全景对比\]- I/O 模型在并发编程中的应用

学习路径建议

  1. 先掌握 I/O 模型(BIO/NIO/AIO)和 ByteBuffer
  2. 学习网络编程(Socket、HTTP、WebSocket),理解 I/O 模型在网络编程中的应用
  3. 学习并发编程(Netty、Reactor),理解 I/O 模型如何支撑高性能并发
相关推荐
孔明兴汉2 小时前
springboot4 项目从零搭建
java·java-ee·springboot
电商API&Tina2 小时前
跨境电商速卖通(AliExpress)数据采集与 API 接口接入全方案
大数据·开发语言·前端·数据库·人工智能·python
黎雁·泠崖2 小时前
指针收官篇:sizeof/strlen + 指针运算笔试考点全梳理
c语言·开发语言
APIshop2 小时前
Java 爬虫 1688 评论 API 接口实战解析
java·开发语言·爬虫
凯子坚持 c2 小时前
Qt 5.14.0 入门框架开发全流程深度解析
开发语言·qt
lingran__2 小时前
数据在内存中的存储详解(C语言拓展版)
c语言·开发语言
编程乐学(Arfan开发工程师)2 小时前
信息收集与分析详解:渗透测试的侦察兵 (CISP-PTE 核心技能)
java·开发语言·javascript·python
bugcome_com2 小时前
深入解析 C# 中 int? 与 int 的核心区别:可空值类型的本质与最佳实践
开发语言·c#
superman超哥2 小时前
仓颉语言中异常处理入门的深度剖析与工程实践
c语言·开发语言·c++·python·仓颉