Ситуация: приложение больше не использует объект открытого файла; почему сборка мусора не гарантирует своевременное освобождение самого файла?
Сборка мусора управляет памятью Java, но не задаёт детерминированный момент освобождения внешних ресурсов: файловых дескрипторов, сокетов или JDBC-соединений. Поэтому объект может стать недостижимым, но файл останется открытым до неопределённого момента; надёжный способ — явно вызвать close(), обычно через try-with-resources.
Управляемая память избавляет Java-программиста от ручного освобождения объектов в куче, однако операционная система и внешние сервисы имеют собственные ресурсы с ограниченным количеством. Файл или сокет нельзя считать освобождённым только потому, что исчезли ссылки на Java-объект.
Исторически для запоздалого освобождения ресурсов применялись финализаторы, но их выполнение не гарантировалось по времени и порядку. Финализация стала нежелательным механизмом и была объявлена устаревшей; основным подходом остаётся явное управление временем жизни ресурса.
После потери последней ссылки объект становится кандидатом на сборку, но сборщик может запуститься значительно позже или не запуститься до завершения процесса. Даже если реализация в итоге обнаружит недостижимый объект и выполнит внутреннюю очистку, это не даёт приложению гарантии, что дескриптор освободится до открытия следующего файла.
При большом числе таких объектов приложение может исчерпать лимит файловых дескрипторов, получить ошибки открытия файлов или удерживать блокировки дольше ожидаемого. Для сетевых соединений последствия аналогичны: соединения могут занимать пул и задерживать обработку новых запросов.
Внешний ресурс нужно закрывать в определённой точке управления потоком. try-with-resources автоматически вызывает close() после тела try, включая обычное завершение, return и выброс исключения.
Здесь возвращаемое значение вычисляется до закрытия ресурса, а сам reader закрывается перед фактическим выходом из метода. Если закрытие выбросит исключение во время уже возникшей ошибки, оно будет добавлено как подавленное; это позволяет сохранить основную причину сбоя.
Cleaner и аналогичные механизмы могут служить аварийным запасным вариантом для нативных ресурсов, но они также не обеспечивают своевременное освобождение. Их нельзя использовать как замену явному close(), особенно для ограниченных или блокирующих ресурсов.
Сервис импортирует тысячи файлов и хранит объекты потоков в локальных переменных до завершения крупных операций. В тестах он работает, но в production периодически получает ошибку исчерпания файловых дескрипторов.
Вариант с надеждой на сборку мусора прост, но не даёт временной гарантии и плохо диагностируется. Ручной close() в нескольких ветвях возможен, однако легко пропустить закрытие при исключении или добавить дублирующее закрытие.
Выбран try-with-resources: он связывает ресурс с областью действия, закрывает его при любом выходе из блока и корректно сохраняет подавленные исключения. После этого число одновременно открытых файлов определяется логикой обработки, а не непредсказуемым расписанием сборщика мусора.
Вопрос: Может ли объект быть недостижимым, но его внешний ресурс ещё оставаться открытым?
Ответ: Да. Недостижимость означает только отсутствие доступного пути от корней сборки мусора к объекту в куче. Она не является событием close() и не устанавливает срок, к которому операционная система освободит связанный дескриптор.
Вопрос: Гарантирует ли вызов close() освобождение ресурса сразу после потери последней ссылки на объект?
Ответ: Нет. Эти события независимы: потеря ссылки лишь делает объект кандидатом на сборку, а close() должен быть вызван явно. Если ресурс нужно освободить в конкретной точке, ссылка на объект не должна использоваться как механизм управления его временем жизни.
Вопрос: Почему блок finally обычно хуже try-with-resources для закрытия нескольких ресурсов?
Ответ: Вручную написанный finally требует отдельно учитывать порядок закрытия, исключения из каждого close() и сохранение исходной ошибки. try-with-resources формализует этот шаблон: ресурсы закрываются в обратном порядке успешного создания, а последующие ошибки закрытия сохраняются как подавленные, если уже есть основное исключение.