這個報錯是 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,否則設不上去。
