IO 与 NIO 排查面试
本页把 IO/NIO 的常见线上问题和面试回答集中整理。详细原理请跳转到对应知识点页。
面试怎么学
mermaid
flowchart TD
A["面试题"] --> B["先给结论"]
B --> C["解释原理"]
C --> D["说商业场景"]
D --> E["补充坑点和排查"]
E --> F["跳转知识点深入理解"]不要只背“BIO 阻塞,NIO 非阻塞”。面试官真正想听的是:为什么阻塞会占线程、Selector 怎么减少线程、Buffer 指针怎么变化、生产中出了问题怎么查。
高频面试题
| 面试题 | 标准回答 | 原理跳转 |
|---|---|---|
| 字节流和字符流区别 | 字节流处理原始二进制数据,适合图片、压缩包、网络报文;字符流处理文本,会涉及字符集编码和解码。文本读写必须显式指定编码,避免依赖平台默认编码。 | 传统 IO、缓冲与字符编码 |
| 为什么要用缓冲流 | 缓冲流把多次小 IO 合并成较少的大 IO,减少系统调用和底层磁盘/网络访问次数。输出缓冲还需要 flush 或 close 才能确保缓冲数据写到底层。 | 缓冲与字符编码 |
| BIO、NIO、AIO 区别 | BIO 是同步阻塞,通常一个连接一个线程;NIO 常见用法是同步非阻塞,通过 Selector 一个线程监听多个 Channel;AIO 是异步 IO,操作完成后通知回调。 | IO总览、Selector多路复用 |
| NIO 三大组件 | Buffer 是缓冲区,Channel 是连接文件或网络的通道,Selector 是多路复用器,用来监听多个 Channel 的事件。 | NIO Buffer与Channel、Selector多路复用 |
Buffer 的 flip 做什么 | flip 把 Buffer 从写模式切换到读模式,本质是 limit=position、position=0。写完数据要读之前必须 flip。 | NIO Buffer与Channel |
clear 和 compact 区别 | clear 重置指针,表示数据处理完可以重新写;compact 保留未读数据并切回写模式,适合半包未处理完的场景。 | NIO Buffer与Channel |
| Selector 为什么能处理多个连接 | Channel 配成非阻塞后注册到 Selector,Selector 监听 accept/read/write 等事件。线程阻塞在 select,有事件才处理,不需要每个连接一个线程。 | Selector多路复用 |
| NIO 是不是异步 | Java NIO 常见网络用法是同步非阻塞,不是异步。读写不会一直阻塞,但数据拷贝仍由调用线程执行;AIO 才是异步完成后回调。 | Selector多路复用 |
| OP_WRITE 为什么会导致 CPU 高 | OP_WRITE 表示底层发送缓冲区可写,大多数时候都可写。如果一直监听,会导致 Selector 不断返回写事件。应该只有存在未写完数据时才注册,写完后移除。 | Selector多路复用 |
| 什么是零拷贝 | 零拷贝是减少用户态和内核态之间数据复制的技术,Java 常见方式有 FileChannel.transferTo/transferFrom、mmap、DirectBuffer。它不是完全没有复制,而是减少不必要复制。 | 零拷贝与大文件 |
| DirectBuffer 为什么会 OOM | DirectBuffer 使用堆外直接内存,不受 -Xmx 直接限制,但受进程内存和 MaxDirectMemorySize 影响。创建过多、释放不及时、Netty ByteBuf 泄漏都可能导致 Direct buffer memory。 | 零拷贝与大文件、JVM内存结构 |
线上问题排查地图
mermaid
flowchart TD
A["IO/NIO 线上异常"] --> B{"现象是什么"}
B -- "乱码" --> C["检查编码和响应头"]
B -- "OOM" --> D{"堆还是直接内存"}
B -- "CPU 高" --> E["检查 Selector 空轮询 / OP_WRITE / 死循环"]
B -- "接口卡住" --> F["检查阻塞 IO / 下游慢 / 线程池满"]
B -- "Too many open files" --> G["检查流、Socket、文件句柄泄漏"]
B -- "大文件慢" --> H["检查分块、缓冲、磁盘、网络、限速"]
C --> I["修正字符集为 UTF-8"]
D --> J["堆 dump / DirectMemory / RSS"]
E --> K["jstack / 线程热点 / Netty 版本"]
F --> L["线程栈 / 超时 / 连接池"]
G --> M["lsof / 资源关闭 / try-with-resources"]
H --> N["压测和链路耗时"]问题一:中文乱码
常见原因:
| 位置 | 风险 |
|---|---|
| 文件读取 | 使用平台默认编码 |
| HTTP 响应 | Content-Type 没指定 charset |
| 数据库连接 | 客户端、连接、表字段编码不一致 |
| CSV/Excel | 文件来源是 GBK,系统按 UTF-8 读 |
排查步骤:
- 确认文件真实编码。
- 确认读取代码是否显式指定字符集。
- 确认 HTTP 请求响应头。
- 确认数据库连接串和表字段编码。
- 确认日志系统编码。
标准回答:
text
乱码本质是编码和解码字符集不一致。文件里保存的是字节,程序读取时要按正确字符集解码。生产中我会统一使用 UTF-8,并且文件读写、HTTP 响应、数据库连接都显式声明编码,避免使用平台默认编码。问题二:大文件 OOM
危险写法:
java
byte[] bytes = Files.readAllBytes(path);排查思路:
- 看 OOM 类型:
Java heap space还是Direct buffer memory。 - 看代码是否一次性读文件到内存。
- 看是否批量导入时把所有行放入 List。
- 看是否上传/下载没有分块。
- 看 GC 日志和 heap dump。
正确设计:
mermaid
flowchart TD
A["打开文件"] --> B["分块读取"]
B --> C["处理当前块"]
C --> D["释放或复用缓冲区"]
D --> E{"读完了吗"}
E -- "否" --> B
E -- "是" --> F["关闭资源"]问题三:Too many open files
这个错误表示进程打开的文件描述符太多。文件、Socket、管道都可能占用 fd。
常见原因:
| 原因 | 说明 |
|---|---|
| 流未关闭 | 异常路径没 close |
| Socket 未关闭 | 客户端断开后服务端没释放 |
| 文件监听过多 | WatchService 或日志文件打开太多 |
| 系统限制太低 | Linux ulimit -n 太小 |
Java 代码优先检查:
java
try (InputStream in = Files.newInputStream(path)) {
// read
}系统层面可以查:
bash
lsof -p <pid> | wc -l
ulimit -nWindows 上可以用 Process Explorer 或句柄查看工具。
问题四:NIO 服务 CPU 高
常见原因:
| 原因 | 解释 |
|---|---|
| 一直监听 OP_WRITE | Socket 大多数时候可写,导致 select 不断返回 |
| Selector 空轮询 | 特定 JDK/系统问题或框架 bug |
| 业务死循环 | 事件循环线程里有错误循环 |
| IO 线程做慢业务 | 队列堆积,事件处理不及时 |
排查:
top找 CPU 高线程。- 转换线程 ID 为十六进制。
jstack找对应线程栈。- 看是否在 Selector 循环、业务循环或编解码逻辑。
- Netty 项目检查版本和是否有阻塞业务跑在 EventLoop。
问题五:接口一直卡住
可能不是 CPU 问题,而是线程在等 IO。
排查步骤:
mermaid
flowchart TD
A["接口卡住"] --> B["jstack 查看线程状态"]
B --> C{"卡在哪里"}
C -- "Socket read" --> D["下游接口慢或没超时"]
C -- "File read/write" --> E["磁盘慢或文件锁"]
C -- "DB driver" --> F["数据库慢或连接池满"]
C -- "Object.wait" --> G["线程池/锁/队列等待"]
D --> H["设置连接超时和读取超时"]
E --> I["检查磁盘 IO 和文件大小"]
F --> J["查慢 SQL 和连接池"]
G --> K["查并发设计"]项目话术:
text
采集平台如果大量接口调用下游医院系统,线程池里的任务可能长期阻塞在 HTTP read 上。排查时我会先看线程栈,如果大量线程卡在 socketRead,就检查下游耗时、连接超时、读取超时和线程池队列,不能只加线程数。面试回答模板
text
IO/NIO 我会从场景说起。普通文件读写和小并发连接,用传统 IO 更简单,重点是缓冲、编码、资源关闭和大文件分块。高并发长连接下,BIO 一个连接一个线程成本太高,所以使用 NIO 的 Channel、Buffer、Selector,一个线程监听多个连接事件。NIO 不是简单 API 替换,它要求理解 Buffer 指针、非阻塞读写、半包粘包和 OP_WRITE。生产中复杂网络服务一般用 Netty,排查时重点看线程栈、文件句柄、直接内存、编码和大文件内存占用。本章小结
IO/NIO 不是孤立知识点。它和线程池、JVM、Netty、HTTP、数据库连接池、文件导入导出都有关。学会它的标准不是能背类名,而是遇到乱码、OOM、CPU 高、接口卡死、文件句柄泄漏时,知道该看哪里、为什么会这样、怎么改。
