打开太多的文件,too many open files (java)
最近线上Java项目要导入100多万条记录。每3天都会报‘打开太多的文件‘错误。ulimit -a命令查看,发现还是默认值,一直以为是这块默认值没有改的问题。叫领导改下,领导说是程序问题。所以每三天就要重启下,然后就可以再导入三天了。最近借助文心一言研究了下。
文心一言说这个错误通常意味着程序试图打开的文件数量超出了操作系统所允许的限制。这个限制是由操作系统的文件描述符(file descriptor)限制设置的。每个打开的文件、网络连接、套接字等都需要一个文件描述符。怪不得每次报这个错误的时候,redis都连接不上。
遂接着问文心一言,怎么查看进程打开了多少个文件描述符。在文心一言的众多方法中选择了lsof命令,不是系统自带的,还要安装。用命令‘lsof -p 端口‘查看了下,竟然打开了3851个文件描述符。好多前天的文件描述符没有释放,线程早都结束了,为什么不释放了,很奇怪。
遂接着问文心一言,JVM垃圾回收为什么没有释放文件描述符。得到的答案是:文件描述符并不属于JVM的内存管理范畴,因此垃圾回收机制不会直接释放文件描述符。文件描述符是操作系统层面上的概念,用于标识打开的文件、网络连接或其他I/O资源。在Java程序中,当你打开一个文件、创建一个网络连接或执行其他需要I/O操作的任务时,操作系统会为你分配一个或多个文件描述符。这些文件描述符的管理和释放是由操作系统和Java的本地方法库(如JNI)共同完成的,而不是由JVM的垃圾回收器负责。看来只能手动close了。
根据‘lsof -p 端口‘查询未释放的文件描述符,找到对应操作文件的代码,的确一个上传图片到云没有关闭流,还有一个查询本地数据库DB文件的方法,没有释放数据库连接和statement,还有一个目录描述符没有释放,但是我目录在finally里面都删除了啊,文件描述符也说这个目录是删除的,但就是没释放。因为操作这个目录的方法是java.nio.files里面的list方法,我想这个方法总不能有问题吧。目录没释放肯定是因为目录里面的文件没有释放造成的。
第二天更新后,发现没有未释放的图片和DB文件描述符了,但是目录描述符还没有释放。随着我导入文件越来越多,目录描述符也越来越多,估计3天后,又是too many open files错误了,我研究了下源码方法,用到了DirectoryStream<Path> 但是在异常块里面关闭的,没有在finally里面关闭。
public static Stream<Path> list(Path dir) throws IOException {
DirectoryStream<Path> ds = Files.newDirectoryStream(dir);
try {
final Iterator<Path> delegate = ds.iterator();
// Re-wrap DirectoryIteratorException to UncheckedIOException
Iterator<Path> it = new Iterator<Path>() {
@Override
public boolean hasNext() {
try {
return delegate.hasNext();
} catch (DirectoryIteratorException e) {
throw new UncheckedIOException(e.getCause());
}
}
@Override
public Path next() {
try {
return delegate.next();
} catch (DirectoryIteratorException e) {
throw new UncheckedIOException(e.getCause());
}
}
};
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.DISTINCT), false)
.onClose(asUncheckedRunnable(ds));
} catch (Error|RuntimeException e) {
try {
//正常不是应该在finally里面关闭吗???????
ds.close();
} catch (IOException ex) {
try {
e.addSuppressed(ex);
} catch (Throwable ignore) {}
}
throw e;
}
}
遂想着重写这个方法吧,类竟然是final的。突然看到return StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.DISTINCT), false).onClose(asUncheckedRunnable(ds));末尾竟然有onClose()方法,竟然有怎么没关闭了,奇怪了,看了下注释,这个是个handler了,要调用list(Path dir)方法的返回值Stream<Path>的close方法,才会触发handler.我恰恰是没有调用,我调用list(Path dir)的时候。直接Files.list(Paths.get(folder)).forEach()。返回值Stream<Path>都没有变量保存。肯定也没有关闭,随保存了返回值,然后finally关闭了。再导入就没有未释放的文件描述符了。