Перейти к основному содержимому

Java IOException «Слишком много открытых файлов»

· 4 мин. чтения

1. Введение

Распространенной ошибкой при работе с файлами в Java является возможность исчерпания доступных файловых дескрипторов.

В этом уроке мы рассмотрим эту ситуацию и предложим два способа избежать этой проблемы.

2. Как JVM обрабатывает файлы

Хотя JVM прекрасно изолирует нас от операционной системы, она делегирует низкоуровневые операции, такие как управление файлами, операционной системе.

Это означает, что для каждого файла, который мы открываем в приложении Java, операционная система выделяет дескриптор файла, чтобы связать файл с нашим процессом Java. Как только JVM завершает работу с файлом, она освобождает дескриптор.

Теперь давайте углубимся в то, как мы можем вызвать исключение.

3. Утечка файловых дескрипторов

Напомним, что для каждой ссылки на файл в нашем Java-приложении у нас есть соответствующий файловый дескриптор в ОС. Этот дескриптор будет закрыт только после удаления экземпляра ссылки на файл. Это произойдет на этапе сборки мусора .

Однако, если ссылка остается активной и открывается все больше и больше файлов, то в конечном итоге у ОС закончатся файловые дескрипторы для выделения. В этот момент он перенаправит эту ситуацию на JVM, что приведет к возникновению исключения IOException .

Мы можем воспроизвести эту ситуацию с помощью короткого модульного теста:

@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");
}
}

В большинстве операционных систем у процесса JVM закончатся файловые дескрипторы до завершения цикла, что вызовет исключение IOException .

Давайте посмотрим, как мы можем избежать этого состояния при правильной обработке ресурсов.

4. Работа с ресурсами

Как мы уже говорили, файловые дескрипторы освобождаются процессом JVM во время сборки мусора .

Но если мы не закрыли нашу ссылку на файл должным образом, сборщик может решить не уничтожать ссылку в то время, оставив дескриптор открытым и ограничив количество файлов, которые мы можем открыть.

Однако мы можем легко решить эту проблему, убедившись, что если мы открываем файл, мы гарантируем, что закроем его, когда он нам больше не нужен.

4.1. Освобождение ссылок вручную

Освобождение ссылок вручную было распространенным способом обеспечения надлежащего управления ресурсами до JDK 8.

Мы не только должны явно закрыть любой файл, который мы открываем , но также гарантировать, что мы делаем это, даже если наш код дает сбой и выдает исключения. Это означает использование ключевого слова 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");
}
}

Поскольку блок finally всегда выполняется, это дает нам возможность правильно закрыть нашу ссылку, тем самым ограничивая количество открытых дескрипторов.

4.2. Использование попытки с ресурсами

JDK 7 предлагает нам более чистый способ удаления ресурсов. Это обычно известно как попытка с ресурсами и позволяет нам делегировать удаление ресурсов, включая ресурс в определение попытки :

@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");
}
}

Здесь мы объявили nonLeakyHandle внутри оператора try . Из-за этого Java закроет для нас ресурс вместо того, чтобы использовать его окончательно.

5. Вывод

Как мы видим, неправильное закрытие открытых файлов может привести к сложному исключению с последствиями для всей нашей программы. При правильном обращении с ресурсами мы можем гарантировать, что эта проблема никогда не возникнет.

Полный исходный код статьи доступен на GitHub .