关于使用Java引起过多打开文件的问题?

6
当审查同事的代码时,发现以下代码:
    BufferedReader br = new BufferedReader(new FileReader(PATH + fileName));
    //...

我想读取一个文件并将这些行连接成一行,但我没有发现任何关闭代码。所以我认为这可能导致资源泄漏,最终导致太多打开的文件错误。为了证明这一点,我编写了一个测试。

for (int i = 0; i < 7168; i++) { // ulimit -n ==> 7168
    BufferedReader br = new BufferedReader(new FileReader("src/main/resources/privateKey/foo.pem"));
    System.out.println(br.readLine());
}
System.in.read();

非常奇怪,一切都正常,没有抛出预期的异常。

在命令行中检查实际打开的文件。

➜  ~ lsof -p 16276 | grep 'foo.pem' | wc -l
    2538

为什么只有2538,而不是7168?

那么出了什么问题?如何导致“打开的文件太多错误”?


正如 @GhostCat 建议的那样,将7168更改为Integer.MAX_VALUE后,这次引发了

java.io.FileNotFoundException: src/main/resources/privateKey/foo.pem (Too many open files in system)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)

当i为27436时,在命令行中检查实际打开的文件。
➜  ~ lsof | grep foo.pem | wc -l
    7275

但是剩下的文件(27346-7275)去哪了?为什么ulimit设置无效呢?


1
我会使用 while(true) 循环来开始这样的实验...也许将计数器打印到每个文件中...并检查磁盘上发生了什么。 - GhostCat
谢谢!请查看我的附加信息。 - zhuguowei
而且更重要的是:你可能忽略了我的建议:我会尝试打开不同的文件进行写入,并且在每个文件中写入不同的内容。除非我的答案完全错误,否则如果你发现你的操作系统能够以某种方式“优化”对同一文件的多次读取请求,那也不会让我感到惊讶。但是将不同的内容写入不同的文件是无法轻易优化的。让我知道这个实验的结果吧;-) - GhostCat
3个回答

7
我认为垃圾回收器正在运行,找到了许多无法访问的BufferedReader对象并对它们进行了收集。这导致底层流对象被终止...从而关闭它们。
要使此代码中断,请将BufferedReader对象添加到列表中,以使它们保持可访问状态。
以下是我认为将7168更改为MAXINT有效的原因。
当JVM启动时,它将使用相对较小的堆。在GC期间发生的事情之一是JVM决定是否需要调整堆大小。因此,以下是可能发生的情况:
  • JVM启动时,堆过小,无法容纳7168个打开文件+ BufferedReader对象。(请记住,每个后者都可能有一个预分配的缓冲区!)

  • 您开始打开文件。

  • 在N = 7168-2538左右,堆填满了所有的BufferedReader对象+ FileInputStream对象+来自JVM启动/热身的各种残留物。

  • GC运行,并导致(可能)所有的BufferedReader对象被收集/终止/关闭。

  • 然后GC决定需要扩展堆。您现在拥有足够的堆空间,可以打开比您的ulimit允许的更多的BufferedReader对象。

  • 您继续打开文件...然后达到打开文件限制。

这是一种可能的模式。
如果您真的想调查此问题,我建议您打开GC日志,并查看lsof报告的FD数是否与GC运行相关。
(您可以尝试在每次打开之间添加sleep调用,以便更轻松地获取lsof测量值。但这可能会以其他方式改变JVM行为...)

我不这么认为,因为每次执行“lsof”命令得到的结果都是相同的。如果涉及垃圾回收,结果会发生变化,即该值将变小直至为0。 - zhuguowei
我同意Stephen的观点。但是为什么不测试一下呢:重写BufferedReader,重写finalize,看看会发生什么...? - GPI
@zhuguowei - 你认为垃圾回收器一直在运行,事实上,它只有在需要时才会运行;例如当“空间”被填满时。因此,您提出的行为不会发生。 - Stephen C

1
  1. jvm implicitly update ulimit value

    String [] cmdArray = {"sh","-c","ulimit -n"};
    Process p = Runtime.getRuntime().exec(cmdArray);
    BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
    System.out.println(in.readLine()); //it is 10240 not 7168
    
  2. @Stephen C is right, GC involved.

我创建了一个MyBufferedReader类,继承自BufferedReader并重写了finalize方法。
@Override
protected void finalize() throws Throwable {
    System.out.printf("Thread: %s finalize it and total: %d %n",Thread.currentThread().getName(),count.getAndAdd(1));
}

收到以下信息。
Thread: Finalizer finalize it and total: 9410 

并且在命令行中

➜  ~ lsof -p 5309 | grep 'taicredit_private_key_pkcs8' | wc -l
     830

and 9410 + 830 = 10240


0

我没有确切的解释,但是有一些额外的想法:我们必须明白,事情并不像表面上看起来那么简单。

关键在于:有几个抽象层参与其中。有JVM和JIT;然后是这些下面的操作系统。

意思是:鉴于这些抽象,期望每个新的BufferReader直接导致另一个文件句柄是太天真了。如果Linux内核在此处介入,并告诉JVM“是的,我打开了那个文件;并为您读取了它;这是它的内容”,那也不足为奇。但实际上,Linux内核知道自上次读取请求以来,该文件未被触及或更改...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接