12.Netty如何解决空轮询的
- JDK epoll 实现有 bug:无事件时 Selector.select() 本应阻塞,但会意外唤醒,导致 CPU 空转到 100%
- 该 bug 在 JDK 1.6 u18 声称修复,但 JDK 1.7、1.8 仍然存在,只是概率降低
- Netty 没有从根本解决,而是用"计数 + 重建 Selector"的方式规避:空轮询超过 512 次就重建 Selector
- 重建过程:新建 Selector → 把旧 Selector 的 SelectionKey 全部迁移过去 → 替换成员变量引用
1. NIO 空轮询 bug 是什么?
JDK 1.5 引入了基于 epoll 的事件响应机制来优化 NIO。epoll 把事件处理交给操作系统内核(硬中断)来处理,解决了 select/poll 模型无效遍历的问题。
但 JDK 的 epoll 实现有个著名的 bug:理论上没有 I/O 事件时,Selector.select() 应该一直阻塞,但 bug 会导致它不断地被意外唤醒,程序陷入死循环,不断向 CPU 申请资源,最终 CPU 飙到 100%。

官方声称在 JDK 1.6 update 18 修复了这个问题,但实际上 JDK 1.7、1.8 中该 bug 仍然存在,只是触发概率降低了,并没有被根本解决。
2. Netty 怎么解决的?
Netty 底层同样调用了 NIO 的 Selector,但它用计数 + 重建 Selector 的方式规避了这个问题。

核心逻辑在 NioEventLoop 的 select 流程里,详见 5.Netty的EventLoop:
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// 轮询持续时间达到预期,说明是正常的阻塞唤醒,重置计数
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// 空轮询次数超过阈值(默认 512),触发 Selector 重建
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
判断逻辑:
- 每次 select 前记录当前时间
currentTimeNanos - select 返回后,检查实际耗时是否达到预期的
timeoutMillis - 达到预期 → 正常唤醒,
selectCnt重置为 1 - 没达到预期 → 可能触发了空轮询,
selectCnt累加 selectCnt超过阈值 512 → 触发 Selector 重建

3. Selector 重建过程
重建分三步:
第一步:新建一个 Selector

开启一个线程,创建全新的 Selector 实例。
第二步:迁移 SelectionKey

把旧 Selector 上已注册的所有 SelectionKey(即所有 Channel 的监听事件)全部迁移到新 Selector 上,保证已有连接不受影响。
第三步:替换引用

把成员变量 this.selector 指向新 Selector,旧 Selector 等待 GC 回收。
flowchart LR
A[selectCnt 累加] -->|超过 512 次| B[新建 Selector]
B --> C[迁移所有 SelectionKey]
C --> D[this.selector 指向新 Selector]
D --> E[旧 Selector 等待 GC]4. 本质是规避,不是修复
Netty 的方案是"用计数检测异常,用重建规避问题",并没有从根本上修复 JDK epoll 的 bug。
空轮询在达到阈值之前仍然会发生,只是超过 512 次后强制重建,把影响控制在可接受范围内。这是一种权衡:重建 Selector 有一定开销,但比 CPU 100% 要好得多。