这个报错是 OS 层面的,不是 Java 代码 bug。Linux 把文件、socket、管道都算作文件描述符(FD),进程能同时打开的 FD 有上限。报错不代表你的代码写错了,而是「句柄开得太多或没及时关」,最常见的陷阱则是一个很多人都踩过的:改了
ulimit却对正在跑的服务根本没生效。
Issue & Env(案发现场与环境)
- 现象:高并发服务运行一段时间后拒绝新连接,日志出现
Too many open files,重启后暂时恢复,隔一阵又复发。 - 环境:Linux,Java 服务,常经 systemd、Docker 或云平台部署。
StackTrace(堆栈日志)
Java 侧常见的两种形态:
Caused by: java.io.IOException: Too many open files
at java.io.UnixFileSystem.createFileExclusively(Native Method)
at java.io.File.createNewFile(File.java:1006)
java.net.SocketException: Too many open files
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:455)
at java.lang.Thread.run(Thread.java:748)
SocketException 说明是连接层面被打满,IOException 说明是文件创建/读写层面。
Reproduce(复现代码)
循环打开文件或 socket 却不关闭,FD 只增不减:
public static void main(String[] args) throws Exception {
List<Socket> sockets = new ArrayList<>();
for (int i = 0; ; i++) {
sockets.add(new Socket("localhost", 8080)); // 每次开一个,从不关闭
}
}
跑上一阵就会撞上进程的 FD 上限,抛 Too many open files。
Root Cause(根因分析)
Linux 对 FD 有三层限制,任何一层被击穿都会报这个错:
- 单进程软限制(
RLIMIT_NOFILEsoft):ulimit -n看到的值,实际生效的就是它,很多发行版默认只有 1024。 - 单进程硬限制(hard):软限制能自己抬到的上限,非特权用户只能往下调。
- 系统级
fs.file-max:全系统所有进程 FD 总和的上限,对应 errno 23(ENFILE),报错信息会写成Too many open files in system。
最大的坑在「改了不生效」。ulimit -n 65536 只是改了当前 shell 的值,跟那个几天前就已经启动的服务毫无关系;limits.conf 又只管登录会话,systemd 管理的服务根本不理它。于是出现「我在 shell 里看是 10 万,跑起来还是 1024」的矛盾。
另一个陷阱是统计口径:lsof -p PID | wc -l 会把工作目录、可执行文件、内存映射库也算进去,数值虚高,真正权威的是 /proc/PID/fd 里实际打开的 FD 数。
代码层面,泄漏基本来自三类:打开的文件/流没关、Socket/连接池的连接没归还、FileChannel 或 RandomAccessFile 没释放。lsof -p PID | grep -c socket 能快速判断是文件句柄还是连接在疯涨。
The Fix(解决方案)
先看真正生效的限制和实际用量:
# 该进程真正生效的 soft/hard 限制
cat /proc/<PID>/limits | grep "open files"
# 实际已打开的 FD 数(最准确)
ls /proc/<PID>/fd | wc -l
# 全系统水位
cat /proc/sys/fs/file-nr
systemd 服务要在单元里改,而不是 limits.conf:
[Service]
LimitNOFILE=65535
然后 systemctl daemon-reload && systemctl restart <服务>。Docker 容器要显式传:docker run --ulimit nofile=65536:65536 ...。
是 FD 泄漏就回到代码,用 try-with-resources 保证流和 channel 关闭,连接池要保证 close() 归还连接,别留着 Socket 到堆里等 GC——GC 回收对象慢,FD 打满会比内存打满更早发生。查泄漏用 lsof -p PID 按文件类型统计,能一眼看出是哪种句柄在涨。
limits 合理、也没泄漏,才考虑是并发确实很高,那就把 soft 限制按连接数加上余量调大,注意单进程 soft 不能超过 fs.nr_open,否则设不上去。
