Java在Linux上默认通过NIO Selector底层绑定epoll实现,关键在于正确配置、非阻塞建模与规避反模式:确认未强制切回poll、验证EPollSelectorImpl类名、监听及新连接SocketChannel均设为非阻塞、循环读写至EAGAIN、合理拆分Selector线程并避免阻塞操作。
java本身不直接调用epoll系统调用,但jdk在linux上默认通过nio的selector底层绑定epoll实现。所谓“压榨吞吐极限”,本质是让java nio线程尽可能贴近epoll的高效能力,避免阻塞、减少拷贝、消除误用瓶颈。关键不在“写epoll代码”,而在**正确配置+合理建模+规避反模式**。
确保JVM真正走epoll路径
Linux下JDK 7+默认启用epoll,但需验证和加固:
- 确认系统未被强制切回poll:检查启动参数是否含
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider,有则删掉 - 运行时验证:用
jstack查看NIO线程栈,若出现EPollArrayWrapper#epollWait或EPollSelectorImpl类名,说明已走epoll - JDK 17+可加
-XX:+UseEPoll显式启用(默认已开,但显式更稳妥)
非阻塞Socket必须全程贯彻
epoll只负责通知就绪,真正不卡住线程靠的是socket本身非阻塞:
- 监听套接字(
ServerSocketChannel)创建后立即设为非阻塞:configureBlocking(false) -
accept()返回的新连接(SocketChannel)必须立刻设为非阻塞——这是最常被忽略的一环 - 所有
read()/write()操作必须循环处理BufferUnderflowException和IOException中errno == EAGAIN/EWOULDBLOCK的情况,不能只读一次就停
采用边缘触发(ET)并一次性读尽
Java NIO虽未暴露EPOLLET开关,但JDK内部在Linux上对注册的channel默认使用ET语义(由EPollSelectorImpl自动适配)。这意味着:
- 读事件触发后,必须循环调用
read()直到返回-1(连接关闭)或0(EAGAIN),否则剩余数据不会再次通知 - 建议使用
ByteBuffer.allocateDirect()减少堆内存拷贝;配合compact()复用buffer,避免频繁分配 - 写操作同理:注册
OP_WRITE仅在写缓冲区满(write()返回0)时才需开启,写完立即取消,防止空转唤醒
线程与Selector模型要匹配硬件
单个Selector能支撑数万连接,但吞吐受CPU核心数制约:
立即学习“Java免费学习笔记(深入)”;
- 不要让一个Selector承担全部压力:按连接类型/业务优先级拆分多个Selector线程(如:1个专管accept,2个分担读写)
- 避免在Selector线程中做耗时操作(如JSON解析、DB访问)——收到数据后应交由业务线程池处理
- 启用TCP快速回收:
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse,缓解TIME_WAIT堆积 - 调大系统文件描述符限制:
ulimit -n 1000000,并检查/proc/sys/fs/file-max
不复杂但容易忽略:极限吞吐不是靠堆参数,而是让每一次epoll_wait返回都对应真实I/O动作,不让通知落空、不因一次读写中断而丢后续数据、不在线程里干杂活。做到这三点,Java在Linux上跑出百万级QPS是可行的。


















