java nio selector空轮询是jdk 1.5–1.7 linux epoll的bug,导致select()高频返回;direct bytebuffer易内存泄漏因cleaner回收不可控;interestops修改需等待下次select才生效,且须线程安全。

Java NIO 的 Selector.select() 为什么总在空转?
因为 JDK 1.5–1.7 在 Linux epoll 实现里存在一个著名 bug:当底层 epoll_wait 返回 0(无事件),但 Selector 仍认为有就绪通道,导致 select() 立即返回,线程陷入空轮询。这不是你代码写错了,是 JVM 自身缺陷。
- 现象:CPU 占用飙到 100%,
select()调用频率异常高(毫秒级反复返回),日志里几乎看不到实际 I/O 事件 - 触发条件:Linux + JDK ≤ 1.7u4(尤其 u21 之前版本),高并发短连接场景更明显
- 临时缓解:在
select()后加Thread.sleep(1)—— 别真用,这只是验证是否为空轮询;正解是升级或绕过 - 根本修复:JDK 1.7u4+ 引入「空轮询补偿机制」,但默认关闭;需启动参数显式开启:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider(仅部分版本有效)
直接堆 ByteBuffer 分配为什么容易内存泄露?
原生 NIO 不做堆外内存生命周期管理,ByteBuffer.allocateDirect() 返回的对象只靠 GC 触发 Cleaner 回收,而 Cleaner 执行时机不可控,且可能被 GC 延迟甚至跳过。
- 典型泄漏点:频繁创建 direct buffer(比如每次读写都 new 一个),又没显式调用
buffer.clear()或复用 buffer,GC 来不及回收时,堆外内存持续增长,OutOfMemoryError: Direct buffer memory - 别信「反正有 Cleaner」—— JDK 9+ 改用
ReferenceQueue,但仍有延迟;JDK 8 及以前 Cleaner 是软引用,极易被忽略 - 安全做法:用池化(如 Netty 的
PooledByteBufAllocator),或至少复用固定数量的ByteBuffer,避免每次 new - 验证方法:用
jstat -gc <pid></pid>观察CCST(Concurrent Class Unload Time)和 direct memory 使用量,或用 Native Memory Tracking(-XX:NativeMemoryTracking=detail)
SelectionKey.interestOps() 修改不生效?
不是 API 失效,而是你没理解「interest set 是异步更新」—— 修改后必须等下一次 select() 才真正通知内核,且中间若发生 key 取消或 channel 关闭,操作会被静默丢弃。
- 常见错误:在 handler 里调用
key.interestOps(SelectionKey.OP_READ)后立刻 expect 下次读事件,结果没触发 - 正确顺序:修改 interestOps → 调用
key.channel().register(selector, newOps, attachment)显式重注册(不推荐),或更稳妥地用key.interestOps(newOps)+ 确保 selector 处于下次 select 周期中 - 注意线程安全:所有对
Selector和SelectionKey的操作必须在同一个线程(通常是 selector 所在线程),跨线程调用wakeup()是唯一安全通信方式 - 性能影响:频繁修改 interestOps 会触发内核 epoll_ctl 调用,比单纯 read/write 开销大得多;应尽量合并状态变更,避免每字节都改一次
为什么 Netty 封装了 NIO 却几乎没人手撸原生 NIO?
不是因为 Netty 更“高级”,而是它把上面三个问题全兜住了:Selector 空轮询用自旋计数+重建 selector 解决;ByteBuffer 泄漏靠池化+引用计数;interestOps 变更通过 task queue 统一调度。你写的每一行原生 NIO,都在重复这些防御逻辑。
立即学习“Java免费学习笔记(深入)”;
- 真实成本:维护一个稳定 NIO 服务端,80% 工作量不在业务逻辑,而在对抗 JDK 版本差异、OS 行为、GC 策略和网络边界条件
- 兼容性陷阱:Windows 上是
Select,Linux 是EPoll,macOS 是KQueue,连OP_WRITE的语义都不一致(Linux 下可写 ≠ 缓冲区有空间) - 调试难度:NIO 错误往往不抛异常,而是静默失败(比如 key 被 cancel 后继续用,
isValid()返回 false 但没人检查)
所以不是“不能用”,是除非你在写 JDK、Netty 或性能敏感中间件,否则花三天调通 Selector 空轮询,不如花两小时接入 Netty 并写完业务。










