Java NIO万级并发的根本原因是Linux下Selector默认绑定epoll,通过epoll_create1()创建独立事件实例,利用红黑树管理注册、就绪链表通知,避免线性扫描,且依赖非阻塞I/O与手动清理selectedKeys。

Java NIO 能处理万级并发连接,根本原因不是 Selector 本身有多“智能”,而是它在 Linux 下默认绑定 epoll 系统调用,绕过了 select/poll 的线性扫描缺陷。关键不在 Java 层怎么写,而在内核如何响应注册、就绪和事件分发。
为什么 Selector.open() 在 Linux 上实际调用的是 epoll_create1()
OpenJDK(21+)中,Selector.open() 最终会走到 EPollSelectorImpl 构造器,里面直接调用 EPoll.create() —— 这是个 JNI 方法,底层就是 epoll_create1(0) 系统调用。这意味着:
- 每个
Selector实例对应一个独立的 epoll 实例(即一个内核 event poll 对象),有自己专属的就绪队列和红黑树 - 不依赖全局 FD 位图(
fd_set),因此没有 1024 限制;现代 Linux 默认支持百万级 FD(受/proc/sys/fs/file-max和进程ulimit -n控制) - 创建时分配的
address是一块固定大小的 native 内存(由eventPollArraySize决定),用于后续epoll_wait()直接填充就绪事件,避免反复 malloc
channel.register(selector, OP_READ) 底层到底做了什么
调用 register() 不是把 Channel “存进 Selector”,而是触发一次 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)。其中:
-
fd是SocketChannel底层 socket 的文件描述符(Linux 中为整数,≥3) -
event.events至少包含EPOLLIN(对应OP_READ),通常还带EPOLLET(边缘触发)标志 -
event.data.ptr指向 JVM 内的SelectionKey对象地址,让内核事件就绪时能快速回溯到 Java 层上下文 - 这个操作将 fd 插入 epoll 实例维护的红黑树,时间复杂度 O(log n),不是遍历
selector.select() 为什么不会随连接数增加而变慢
select() 方法阻塞时,实际执行的是 epoll_wait(epfd, events, maxEvents, timeout)。它的高效来自三点:
- 内核只返回“真正就绪”的 fd 列表(比如 10000 个连接里只有 3 个有数据可读),Java 层拿到的
selectedKeys()集合大小≈就绪数,不是总连接数 - 就绪事件通过就绪链表(ready list)通知,无需遍历全部注册 fd;红黑树仅用于管理注册关系,不参与就绪判定
- 使用
EPOLLET后,内核只在 fd 状态从“未就绪→就绪”时通知一次,避免重复唤醒(对比EPOLLONESHOT更轻量)
容易被忽略的关键约束:非阻塞 + 手动清理 selectedKeys
很多人写出高并发 bug,不是因为不懂 epoll,而是忽略了 Java NIO 的两个强制契约:
-
SocketChannel.configureBlocking(false)必须在register()前设置,否则epoll_ctl会失败(Linux 不允许将阻塞 fd 加入 epoll) -
selectedKeys()返回的是一个内部复用的HashSet,每次select()返回后必须显式调用keyIterator.remove()或selectedKeys().clear(),否则下次select()会重复返回已处理过的 key(因为内核没清,Java 层也没清) - 如果 channel 关闭但没 cancel 对应
SelectionKey,其 fd 仍留在 epoll 红黑树中,造成 fd 泄漏,最终 hitEMFILE错误
真正的性能瓶颈往往不出现在 epoll 本身,而出现在 Java 层对就绪 key 的处理逻辑是否阻塞、buffer 是否复用、direct buffer 是否被 GC 干扰——这些才是压测时连接数上万后抖动或吞吐掉档的根源。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











