一、字节流与字符流
Q1:字节流和字符流的区别是什么?
答:
核心区别:字节流面向二进制,字符流面向文本并处理编码。
Q2:为什么需要字符流?字节流不能处理文本吗?
答: 字节流可以读写文本,但需要手动处理编码转换,容易出错:
// 字节流读文本,需手动解码
InputStream in = new FileInputStream("a.txt");
byte[] buf = new byte[1024];
int len = in.read(buf);
String s = new String(buf, 0, len, StandardCharsets.UTF_8);字符流封装了编码解码:
Reader reader = new InputStreamReader(
new FileInputStream("a.txt"), StandardCharsets.UTF_8);字符流 = 字节流 + 编码表,是字节流的包装。
Q3:InputStreamReader 和 OutputStreamWriter 的作用是什么?
答: 它们是字节流和字符流之间的桥梁:
// 字节流 → 字符流
Reader r = new InputStreamReader(
new FileInputStream("a.txt"), "UTF-8");
// 字符流 → 字节流
Writer w = new OutputStreamWriter(
new FileOutputStream("a.txt"), "UTF-8");InputStreamReader:把字节按指定编码解码成字符OutputStreamWriter:把字符按指定编码编码成字节
Q4:FileReader / FileWriter 有什么坑?
答: 它们使用平台默认编码,跨平台会乱码:
// 不推荐,编码依赖系统
FileReader fr = new FileReader("a.txt");
// 推荐,显式指定编码
Reader r = new InputStreamReader(
new FileInputStream("a.txt"), StandardCharsets.UTF_8);在中文 Windows 上是 GBK,Linux 上通常是 UTF-8,容易出问题。
Q5:什么是缓冲流?为什么需要它?
答: 缓冲流(BufferedInputStream、BufferedReader 等)在内存中维护缓冲区,减少系统调用次数:
// 无缓冲:每次 read 都可能触发系统调用
InputStream in = new FileInputStream("a.txt");
// 有缓冲:一次读 8KB,减少 IO 次数
InputStream in = new BufferedInputStream(
new FileInputStream("a.txt"), 8192);核心价值:磁盘/网络 IO 是慢操作,缓冲能显著提升吞吐量。
Q6:flush() 和 close() 有什么区别?
答:
flush():强制把缓冲区数据写入目标,流仍可继续使用close():先 flush,再释放资源,之后流不可用
Writer w = new BufferedWriter(new FileWriter("a.txt"));
w.write("hello");
w.flush(); // 立即写入,不关闭
w.write("world");
w.close(); // 关闭并释放注意:close() 通常隐含 flush(),但某些包装流不保证。
Q7:如何用字节流读取一行?和 BufferedReader.readLine() 有何不同?
答: 字节流没有 readLine(),需要自己实现:
public static String readLine(InputStream in) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int b;
while ((b = in.read()) != -1) {
if (b == '\n') break;
if (b != '\r') bos.write(b);
}
return bos.size() == 0 && b == -1 ? null
: bos.toString("UTF-8");
}BufferedReader.readLine() 内部就是类似逻辑,但按字符处理,自动处理 \r\n。
二、BIO / NIO / AIO
Q8:BIO、NIO、AIO 分别是什么?
答:
核心区别:
BIO:
read()阻塞直到有数据NIO:
read()无数据立即返回,用 Selector 管理多个连接AIO:
read()立即返回,数据就绪后回调通知
Q9:BIO 的模型是怎样的?有什么问题?
答: 典型的一请求一线程模型:
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept(); // 阻塞
new Thread(() -> {
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
in.read(buf); // 阻塞
// 处理...
}).start();
}问题:
每个连接一个线程,线程开销大
连接数多时线程爆炸
accept()和read()都阻塞,CPU 利用率低
Q10:NIO 的三大核心组件是什么?
答:
Channel:双向通道,可读可写
Buffer:数据容器,读写都经过 Buffer
Selector:多路复用器,一个线程管理多个 Channel
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞直到有事件
Set<SelectionKey> keys = selector.selectedKeys();
// 处理事件...
}Q11:NIO 为什么比 BIO 高效?
答:
BIO:一个连接一个线程,线程上下文切换开销大
NIO:一个线程管理多个连接(多路复用),连接数不受线程数限制
本质:NIO 用少量线程 + 事件驱动,替代大量阻塞线程。
但注意:NIO 编程复杂,且若业务处理慢,仍会阻塞 Selector 线程。
Q12:AIO 和 NIO 的区别是什么?
答:
// AIO 示例
AsynchronousSocketChannel channel =
AsynchronousSocketChannel.open();
channel.read(buffer, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
// 读完成后回调
}
@Override
public void failed(Throwable exc, Object attachment) {}
});注意:Linux 上 AIO 底层仍是 epoll 模拟,性能优势不明显,Netty 也用 NIO 而非 AIO。
Q13:Netty 为什么选择 NIO 而不是 AIO?
答:
Linux 原生 AIO 不成熟,JDK AIO 底层用 epoll 模拟
AIO 回调编程模型复杂,调试困难
NIO + epoll 在多路复用场景下性能足够
Netty 通过 Reactor 模式封装 NIO,兼顾性能和可控性
三、Channel / Buffer / Selector
Q14:Channel 和 Stream 有什么区别?
答:
// Stream:单向
InputStream in = ...; // 只能读
OutputStream out = ...; // 只能写
// Channel:双向
SocketChannel ch = ...;
ch.read(buffer);
ch.write(buffer);Q15:Buffer 的核心属性有哪些?
答: 四个核心属性:
关键方法:
flip():写模式 → 读模式(limit=position, position=0)clear():读模式 → 写模式(position=0, limit=capacity)rewind():position=0,可重读compact():把未读数据移到头部,准备继续写
Q16:为什么 Buffer 需要 flip()?
答: 因为 Buffer 读写共用 position:
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("hello".getBytes()); // position=5
// 不 flip 直接读,position=5,读不到数据
buf.flip(); // limit=5, position=0
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}flip 的本质:把"写到的位置"变成"读的上限",position 归零。
Q17:直接缓冲区(DirectBuffer)和堆缓冲区(HeapBuffer)有什么区别?
答:
ByteBuffer heap = ByteBuffer.allocate(1024); // 堆
ByteBuffer direct = ByteBuffer.allocateDirect(1024); // 堆外DirectBuffer 适合长期、频繁 IO 场景,但创建销毁成本高。
Q18:Selector 的工作原理是什么?
答: Selector 基于操作系统的多路复用(Linux epoll、Windows select):
把 Channel 注册到 Selector,关注特定事件
select()阻塞等待事件就绪返回就绪的 SelectionKey 集合
遍历处理事件
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> it =
selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isAcceptable()) { /* 处理连接 */ }
if (key.isReadable()) { /* 处理读 */ }
it.remove(); // 必须移除
}
}Q19:Selector 支持哪些事件?
答: 四种:
注意:OP_WRITE 通常不常注册,因为大部分时间可写,会导致忙轮询。
Q20:为什么处理完事件后要 it.remove()?
答: selectedKeys() 返回的集合不会自动清除,处理完必须手动移除,否则下次 select() 会重复处理:
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
// 处理...
it.remove(); // 关键
}不 remove 会导致事件重复处理、逻辑错误。
Q21:Selector 是线程安全的吗?
答: 不是。多个线程同时操作 Selector 会有问题:
register()会阻塞在select()的线程推荐一个 Selector 由一个线程管理
Netty 用 EventLoop 保证每个 Selector 单线程处理
如需唤醒 select(),调用 selector.wakeup()。
四、零拷贝(Zero-Copy)
Q22:什么是零拷贝?
答: 零拷贝指减少或消除 CPU 在数据拷贝上的开销,让数据尽量不经过用户空间,直接在内核空间传输。
传统 IO 的 4 次拷贝 + 4 次上下文切换:
磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡
(DMA) (CPU拷贝) (CPU拷贝) (DMA)零拷贝目标:减少 CPU 拷贝和上下文切换。
Q23:传统文件读取发送到网络,经历了几次拷贝?
答: 4 次拷贝、4 次上下文切换:
// 传统方式
byte[] buf = new byte[1024];
while ((len = fileInputStream.read(buf)) != -1) {
socketOutputStream.write(buf, 0, len);
}流程:
DMA:磁盘 → 内核页缓存
CPU:内核页缓存 → 用户缓冲区
CPU:用户缓冲区 → Socket 缓冲区
DMA:Socket 缓冲区 → 网卡
Q24:零拷贝有哪些实现方式?
答: 主要有四种:
Q25:mmap + write 的原理是什么?
答:
MappedByteBuffer buf =
fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
socketChannel.write(buf);mmap()把内核页缓存映射到用户空间用户空间和内核空间共享同一块内存
省去"内核 → 用户"的拷贝
拷贝次数:磁盘 → 页缓存(DMA)→ Socket 缓冲区(CPU)→ 网卡(DMA),共 3 次。
缺点:mmap 有内存映射开销,且文件过大时映射成本高。
Q26:sendfile 的原理是什么?
答:
// Java 中通过 FileChannel.transferTo 使用
fileChannel.transferTo(position, count, socketChannel);sendfile() 让数据不经过用户空间:
磁盘 → 内核页缓存 → Socket 缓冲区 → 网卡
(DMA) (CPU拷贝) (DMA)Linux 2.4 后,如果网卡支持 SG-DMA(Scatter-Gather DMA),可进一步省去 CPU 拷贝:
磁盘 → 内核页缓存 → 网卡
(DMA) (SG-DMA)真正的零拷贝:2 次 DMA 拷贝,0 次 CPU 拷贝。
Q27:Java 中如何使用零拷贝?
答: 主要通过 FileChannel.transferTo() 和 transferFrom():
// 服务端发送文件
FileChannel fileChannel = new FileInputStream("a.txt").getChannel();
SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("localhost", 8080));
// 零拷贝发送
fileChannel.transferTo(0, fileChannel.size(), socketChannel);底层:Linux 上用 sendfile(),Windows 上用 TransmitFile()。
Q28:Kafka 和 Netty 如何利用零拷贝?
答:
Kafka:
用
transferTo()把日志文件发送给消费者用
mmap()读写索引文件大幅减少 CPU 拷贝和上下文切换
Netty:
提供
FileRegion,底层用FileChannel.transferTo()提供
CompositeByteBuf,逻辑合并多个 Buffer,避免拷贝ByteBuf.slice()共享底层内存,不拷贝
// Netty 零拷贝发送文件
FileRegion region = new DefaultFileRegion(
new FileInputStream("a.txt").getChannel(), 0, size);
ctx.writeAndFlush(region);Q29:零拷贝一定比传统 IO 快吗?
答: 不一定,要看场景:
适合:大文件传输、静态资源服务、Kafka 日志推送
不适合:需要对数据做修改(如加密、压缩),因为零拷贝数据不经过用户空间
小文件:优势不明显,甚至因系统调用开销更慢
零拷贝的核心是减少 CPU 拷贝,不是万能优化。
Q30:什么是"广义零拷贝"?
答: 除了内核层面的零拷贝,还包括减少数据在内存中的复制:
Netty 的 CompositeByteBuf:逻辑合并多个 Buffer,不物理拷贝
ByteBuf.slice():共享底层数组,只改指针
Java 的 DirectByteBuffer:堆外内存,避免堆内到堆外的拷贝
字符串常量池:复用已有对象
这些都属于广义的"减少拷贝"思想。
评论