Объект стал недостижимым, но пережил первую сборку мусора. Как механизм финализации объясняет его временное сохранение?
Если у недостижимого объекта переопределён метод finalize(), JVM может сначала поместить объект на обработку финализатору, а не освободить его память. После выполнения финализатора объект обычно становится кандидатом для следующей сборки, но может быть воскрешён, если финализатор снова сохранит ссылку на него из корня GC.
Финализация появилась как способ автоматически выполнить завершающие действия для объектов, особенно связанных с нативными ресурсами. Она должна была уменьшить зависимость от явного управления памятью и ресурсами.
На практике финализация оказалась ненадёжной: момент вызова finalize() не определён, финализатор может задерживаться или не успеть выполниться до завершения JVM. Поэтому механизм устарел и в современных Java-приложениях предпочтительны try-with-resources, явное закрытие ресурсов и, в отдельных случаях, Cleaner.
Недостижимость обычной ссылки не всегда означает немедленное освобождение объекта. Если объект требует финализации, JVM должна сначала обеспечить возможность вызова его финализатора, что увеличивает время жизни объекта и давление на память.
Ошибочное ожидание своевременного вызова finalize() приводит к утечкам файловых дескрипторов, сокетов или нативной памяти. Дополнительный риск — воскрешение объекта: финализатор может сделать объект снова достижимым, поэтому его память нельзя освобождать после первой обнаруженной недостижимости.
При обнаружении недостижимого объекта с финализатором JVM не освобождает его немедленно. В реализации HotSpot такой объект связывается со специальной очередью, а отдельный поток финализации позднее вызывает finalize(); точные детали реализации не следует считать универсальным контрактом Java.
После выполнения финализатора объект не финализируется повторно. Если объект не был воскрешён, он остаётся недостижимым и может быть освобождён следующей сборкой мусора. Если финализатор записал this в статическое поле, живую структуру данных или другой GC-корень, объект снова становится достижимым и переживает сборку.
Пример показывает именно воскрешение:
После потери внешних ссылок объект может попасть на финализацию, а затем остаться живым через LegacyResource.saved. Повторная потеря этой ссылки уже не вызовет finalize() второй раз, поэтому объект в конечном итоге будет собран без повторного финализатора.
Финализация также создаёт задержки и дополнительную нагрузку: объекты сначала переживают обнаружение недостижимости, затем обрабатываются отдельным потоком. Исключение из финализатора не превращает механизм в надёжный способ освобождения ресурса и не гарантирует своевременное закрытие ресурса.
Сервис использовал класс-обёртку над нативным дескриптором и закрывал дескриптор только в finalize(). При пиковом трафике сборщик находил много недостижимых обёрток, но поток финализации не успевал обрабатывать их, из-за чего заканчивались дескрипторы.
Рассматривались три варианта. Увеличение лимита дескрипторов давало временный эффект, но не устраняло задержку освобождения. Замена финализации на Cleaner сохраняла автоматическую страховку, однако также не давала детерминированного момента очистки. Лучшим решением стало сделать ресурс AutoCloseable, закрывать его через try-with-resources, а Cleaner оставить только как аварийный резерв.
В результате ресурс освобождался в конце логической операции, а автоматическая очистка перестала быть частью нормального жизненного цикла. Это снизило число одновременно занятых дескрипторов и устранило зависимость от скорости финализатора.
finalize() для каждого такого объекта?Нет, своевременного вызова не гарантируется. JVM может завершиться до обработки очереди финализаторов, а доступность и поведение финализации зависят от версии Java и конкретной реализации. Поэтому finalize() нельзя использовать как единственный механизм освобождения критического ресурса.
Это зависит от алгоритма и реализации сборщика, но семантически после выполнения финализатора объект должен считаться обработанным, а если он не был воскрешён — может стать кандидатом на освобождение. Нельзя рассчитывать на конкретное число сборок или на немедленное уменьшение потребления памяти.
Cleaner не является полной заменой детерминированному закрытию ресурса?Cleaner также работает асинхронно: очистка выполняется после того, как объект станет недостижимым, поэтому момент очистки не определён. Он полезен как защитный механизм от ошибки вызывающего кода, но для файлов, соединений и других ограниченных ресурсов основным способом остаётся явное закрытие, обычно через try-with-resources.