Java IOException „Твърде много отворени файлове“

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.

Не само трябва изрично да затворим какъвто и да е файл, който отворим , но също така да гарантираме, че го правим, дори ако нашият код се провали и изхвърля изключения. Това означава, използващи най-накрая ключова дума:

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

Тъй като окончателният блок винаги се изпълнява, той ни дава възможност да затворим правилно референцията си, като по този начин ограничим броя на отворените дескриптори.

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

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

Тук декларирахме nonLeakyHandle вътре в инструкцията try . Поради това Java ще затвори ресурса вместо нас, вместо да се налага да използваме накрая.

5. Заключение

Както виждаме, неуспехът при правилното затваряне на отворени файлове може да ни доведе до сложно изключение с последствия в цялата ни програма. С правилното боравене с ресурси можем да гарантираме, че този проблем никога няма да се появи.

Пълният изходен код на статията е достъпен в GitHub.