1. Introduction
1.绪论
A common pitfall when working with files in Java is the possibility of running out of available file descriptors.
在Java中处理文件时,一个常见的陷阱是可能会耗尽可用的文件描述符。
In this tutorial, we’ll take a look at this situation and offer two ways to avoid this problem.
在本教程中,我们将看一下这种情况,并提供两种方法来避免这个问题。
2. How the JVM Handles Files
2.JVM如何处理文件
Although the JVM does an excellent job isolating us from the operating system, it delegates low-level operations like file management to the OS.
尽管JVM在将我们与操作系统隔离方面做得很好,但它将文件管理等低级操作委托给操作系统。
This means that for each file we open in a Java application, the operating system will allocate a file descriptor to relate the file to our Java process. Once the JVM finishes with the file, it releases the descriptor.
这意味着,对于我们在Java应用程序中打开的每个文件,操作系统将分配一个文件描述符,以将该文件与我们的Java进程联系起来。一旦JVM完成了对文件的处理,它就会释放这个描述符。
Now, let’s dive into how we can trigger the exception.
现在,让我们深入探讨如何触发异常。
3. Leaking File Descriptors
3.泄漏的文件描述符
Recall that for every file reference in our Java application, we have a corresponding file descriptor in the OS. This descriptor will be closed only when the file reference instance is disposed of. This will happen during the Garbage Collection phase.
回顾一下,对于我们Java应用程序中的每个文件引用,我们在操作系统中有一个相应的文件描述符。这个描述符只有在文件引用实例被处理掉后才会被关闭。这将在垃圾回收阶段发生。
However, if the reference remains active and more and more files are being open, then eventually the OS will run out of file descriptors to allocate. At that point, it will forward this situation to the JVM, which will result in an IOException being thrown.
然而,如果引用保持活跃,并且越来越多的文件被打开,那么最终操作系统将耗尽可分配的文件描述符。这时,它将把这种情况转发给JVM,这将导致抛出一个IOException。
We can reproduce this situation with a short unit test:
我们可以通过一个简短的单元测试来重现这种情况。
@Test
public void whenNotClosingResoures_thenIOExceptionShouldBeThrown() {
try {
for (int x = 0; x < 1000000; x++) {
FileInputStream leakyHandle = new FileInputStream(tempFile);
}
fail("Method Should Have Failed");
} catch (IOException e) {
assertTrue(e.getMessage().containsIgnoreCase("too many open files"));
} catch (Exception e) {
fail("Unexpected exception");
}
}
On most operating systems, the JVM process will run out of file descriptors before completing the loop, thereby triggering the IOException.
在大多数操作系统上,JVM进程会在完成循环之前耗尽文件描述符,从而触发IOException。
Let’s see how can we avoid this condition with proper resource handling.
让我们看看如何通过适当的资源处理来避免这种情况。
4. Handling Resources
4.处理资源
As we said before, file descriptors are released by the JVM process during Garbage Collection.
正如我们之前所说,文件描述符是由JVM进程在垃圾回收期间释放的。
But if we didn’t close our file reference properly, the collector may choose not to destroy the reference at the time, leaving the descriptor open and limiting the number of files we could open.
但是,如果我们没有正确地关闭我们的文件引用,收集器可能会选择不在当时销毁引用,使描述符处于开放状态,限制了我们可以打开的文件数量。
However, we can easily remove this problem by making sure that if we open a file, we ensure we close it when we no longer need it.
然而,我们可以通过确保如果我们打开一个文件,我们确保在我们不再需要它时关闭它,从而轻松地消除这个问题。
4.1. Manually Releasing References
4.1.手动释放引用
Manually releasing references was a common way to ensure proper resource management before JDK 8.
在JDK 8之前,手动释放引用是确保正确管理资源的一种常见方式。
Not only do we have to explicitly close whatever file we open, but also ensure that we do it even if our code fails and throws exceptions. This means using the finally keyword:
我们不仅要明确地关闭我们打开的任何文件,还要确保即使我们的代码失败并抛出异常,我们也要这样做。这意味着使用finally关键字。
@Test
public void whenClosingResoures_thenIOExceptionShouldNotBeThrown() {
try {
for (int x = 0; x < 1000000; x++) {
FileInputStream nonLeakyHandle = null;
try {
nonLeakyHandle = new FileInputStream(tempFile);
} finally {
if (nonLeakyHandle != null) {
nonLeakyHandle.close();
}
}
}
} catch (IOException e) {
assertFalse(e.getMessage().toLowerCase().contains("too many open files"));
fail("Method Should Not Have Failed");
} catch (Exception e) {
fail("Unexpected exception");
}
}
As the finally block is always executed, it gives us the chance to properly close our reference, thereby limiting the number of open descriptors.
由于finally块总是被执行,它让我们有机会正确关闭我们的引用,从而限制开放描述符的数量。
4.2. Using try-with-resources
4.2.使用try-with-resources
JDK 7 brings us a cleaner way to perform resource disposal. It’s commonly known as try-with-resources and allows us to delegate the disposing of resources by including the resource in the try definition:
JDK 7 为我们带来了一种更简洁的方式来执行资源处置。它通常被称为try-with-resources,它允许我们通过在try定义中包含资源来委托处置资源。
@Test
public void whenUsingTryWithResoures_thenIOExceptionShouldNotBeThrown() {
try {
for (int x = 0; x < 1000000; x++) {
try (FileInputStream nonLeakyHandle = new FileInputStream(tempFile)) {
// do something with the file
}
}
} catch (IOException e) {
assertFalse(e.getMessage().toLowerCase().contains("too many open files"));
fail("Method Should Not Have Failed");
} catch (Exception e) {
fail("Unexpected exception");
}
}
Here, we declared nonLeakyHandle inside the try statement. Because of that, Java will close the resource for us instead of us needing to use finally.
在这里,我们在try 语句中声明了nonLeakyHandle。正因为如此,Java将为我们关闭资源,而不是我们需要使用finally.。
5. Conclusion
5.总结
As we can see, failure to properly close open files can lead us to a complex exception with ramifications all across our program. With proper resource handling, we can ensure this problem will never present itself.
正如我们所看到的,如果不能正确地关闭打开的文件,会导致我们出现一个复杂的异常,对整个程序产生影响。通过适当的资源处理,我们可以确保这个问题永远不会出现。
The complete source code for the article is available over on GitHub.
文章的完整源代码可在GitHub上获得。