JEP 421: Deprecate Finalization for Removal
Пометка финализации как Deprecated for Removal (устаревший, будет удалён)
| Authors | Brent Christian, Stuart Marks |
| Ответственный | Brent Christian |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 18 |
| Компонент | core-libs / java.lang |
| Обсуждение | core dash libs dash dev at openjdk dot java dot net |
| Трудоёмкость | S |
| Длительность | S |
| Рецензенты | Alex Buckley, Brian Goetz, Kim Barrett |
| Одобрен | Brian Goetz, Mikael Vidstedt |
| Создан | 2021/09/30 20:24 |
| Обновлён | 2025/03/07 15:16 |
| Задача | 8274609 |
Аннотация
Пометить финализацию как Deprecated for Removal, чтобы удалить её в одном из будущих выпусков. Пока финализация по-прежнему включена по умолчанию, но её можно отключить, чтобы заранее начать тестирование. В одном из будущих выпусков она будет по умолчанию отключена, а в более позднем выпуске — удалена. Тем, кто сопровождает библиотеки и приложения, зависящие от финализации, следует рассмотреть переход на другие способы управления ресурсами, например на оператор try-with-resources и cleaners.
Цели
-
Помочь разработчикам понять, чем опасна финализация.
-
Подготовить разработчиков к удалению финализации в одной из будущих версий платформы Java.
-
Предоставить простые инструменты, которые помогают обнаружить зависимость от финализации.
Мотивация
Утечки ресурсов
В программах на Java память управляется автоматически: сборщик мусора (GC) в JVM освобождает память, занятую объектом, когда объект больше не нужен. Однако некоторые объекты представляют ресурс, предоставленный операционной системой, например открытый файловый дескриптор или блок нативной памяти. Для таких объектов недостаточно просто освободить память объекта: программа должна также вернуть операционной системе стоящий за ним ресурс, как правило вызвав метод close этого объекта. Если программа не сделает этого до того, как GC освободит объект, информация, необходимая для освобождения ресурса, будет потеряна. Ресурс, который операционная система по-прежнему считает используемым, утёк.
Утечки ресурсов могут встречаться на удивление часто. Рассмотрим следующий код, который копирует данные из одного файла в другой. В ранних версиях Java разработчики обычно использовали конструкцию try-finally, чтобы гарантировать освобождение ресурсов, даже если при копировании возникнет исключение:
FileInputStream input = null;
FileOutputStream output = null;
try {
input = new FileInputStream(file1);
output = new FileOutputStream(file2);
... copy bytes from input to output ...
output.close(); output = null;
input.close(); input = null;
} finally {
if (output != null) output.close();
if (input != null) input.close();
}
Этот код содержит ошибку: если при копировании возникнет исключение и оператор output.close() в блоке finally тоже выбросит исключение, произойдёт утечка входного потока. Обрабатывать исключения на всех возможных путях выполнения трудоёмко, и сделать это правильно сложно. (Исправление здесь требует вложенной конструкции try-finally и оставлено читателю в качестве упражнения.) Даже если необработанное исключение возникает лишь изредка, утёкшие ресурсы со временем могут накапливаться.
Финализация — и её недостатки
Финализация, появившаяся в Java 1.0, была задумана для того, чтобы помочь избежать утечек ресурсов. Класс может объявить финализатор — метод protected void finalize(), — тело которого освобождает все связанные с объектом ресурсы. GC планирует вызов финализатора недостижимого объекта перед тем, как освободить его память; метод finalize, в свою очередь, может выполнить такие действия, как вызов метода close этого объекта.
На первый взгляд это выглядит эффективной страховкой от утечек ресурсов: если объект, содержащий всё ещё открытый ресурс, становится недостижимым (как объект input выше), GC запланирует вызов финализатора, который закроет ресурс. По сути, финализация использует возможности сборки мусора для управления ресурсами, не являющимися памятью (Barry Hayes, Finalization in the Collector Interface, International Workshop on Memory Management, 1992).
К сожалению, у финализации есть несколько критических, фундаментальных недостатков:
-
Непредсказуемая задержка — между моментом, когда объект становится недостижимым, и моментом вызова его финализатора может пройти сколь угодно много времени. Более того, GC вообще не гарантирует, что какой-либо финализатор когда-либо будет вызван.
-
Неограниченное поведение — код финализатора может выполнять любые действия. В частности, он может сохранить ссылку на финализируемый объект, тем самым воскресив объект и снова сделав его достижимым.
-
Всегда включена — у финализации нет явного механизма регистрации. Класс с финализатором включает финализацию для каждого своего экземпляра, нужна она или нет. Финализацию объекта нельзя отменить, даже если для этого объекта она больше не нужна.
-
Неопределённые потоки — финализаторы выполняются в неопределённых потоках, в произвольном порядке. Ни потоками, ни порядком управлять нельзя.
Эти недостатки были широко известны ещё более двадцати лет назад. Совет осторожно обращаться с финализацией в Java появился уже в 1998 году (Bill Venners, Object Finalization and Cleanup: How to Design Classes for Proper Object Cleanup, JavaWorld, май 1998) и занимал заметное место в книге Joshua Bloch Effective Java 2001 года (Item 6: «Avoid finalizers»). SEI CERT Oracle Coding Standard for Java рекомендует не использовать финализаторы с 2008 года.
Последствия на практике
В совокупности недостатки финализации приводят на практике к серьёзным проблемам с безопасностью, производительностью, надёжностью и сопровождаемостью.
-
Уязвимости безопасности — если у класса есть финализатор, новый экземпляр этого класса становится кандидатом на финализацию, как только начинает выполняться его конструктор. Если конструктор выбросит исключение, новый экземпляр не уничтожается, хотя может быть инициализирован не полностью. Новый экземпляр остаётся кандидатом на финализацию, и его финализатор может выполнять над объектом произвольные действия, в том числе воскресить его для последующего использования. Вредоносный код может с помощью этого приёма создавать некорректно сформированные объекты, вызывающие неожиданные ошибки, или сбивать с толку в остальном корректный код, заставляя его работать неправильно. (Те же уязвимости относятся к объектам, созданным при десериализации.)
Просто не объявлять финализатор в классе недостаточно, чтобы предотвратить эту проблему. Подкласс может объявить финализатор и тем самым получить доступ к некорректно сконструированным или десериализованным объектам. Чтобы смягчить эту проблему, нужны дополнительные шаги, которые были бы не нужны, если бы финализация не входила в платформу. Эта проблема и способы её смягчения описаны в Oracle Secure Coding Guidelines for Java SE (см. 4-5, «Limit the extensibility of classes and methods», и 7-3, «Defend against partially initialized instances of non-final classes»).
-
Производительность — само наличие финализаторов снижает производительность: GC должен выполнять дополнительную работу при создании объектов, а также до и после их финализации. Например, Hans Boehm описал замедление в 7–11 раз при добавлении финализации в класс. Финализация также приводит к увеличению времени паузы у сборщиков, ориентированных на пропускную способность, и к росту накладных расходов на структуры данных у сборщиков с низкой задержкой.
Некоторые классы на всякий случай предоставляют как явный метод освобождения ресурсов, например
close, так и финализатор. Если пользователь забудет вызватьclose, ресурс может освободить финализатор. Однако поскольку финализаторы не поддерживают отмену, потери производительности возникают всегда, даже для ненужных финализаторов уже освобождённых ресурсов. -
Ненадёжное выполнение — приложения, использующие финализаторы, больше подвержены периодическим и трудно диагностируемым сбоям. Запуск финализаторов планирует GC, но GC обычно работает только тогда, когда это необходимо для выполнения запросов на выделение памяти. Если свободной памяти много, GC может запускаться редко, что вносит произвольную задержку в финализацию. Когда в куче накапливается много объектов, владеющих ресурсами и ожидающих финализации, в результате может возникнуть нехватка ресурсов, которая непредсказуемо ломает приложение. Кроме того, финализация выполняется в неопределённом количестве потоков, поэтому потоки приложения могут выделять ресурсы быстрее, чем потоки финализаторов успевают их освобождать, что опять же приводит к нехватке ресурсов.
-
Сложная модель программирования — корректно реализовать финализаторы на удивление трудно. Как правило, финализатор класса должен вызывать финализатор своего суперкласса, потому что иначе могут возникнуть утечки ресурсов. Разработчик сам должен помнить о вызове
super.finalize()и обрабатывать любые исключения. Компилятор Java не вставляет этот вызов в финализатор автоматически, в отличие от того, как он автоматически вставляет вызовsuper(...)в конструктор.Чтобы предотвратить проблемы, недостаточно обеспечить корректность финализаторов в собственном коде. Другие компоненты могут создавать подклассы вашего кода и переопределять ваши финализаторы. Некорректная реализация финализатора с их стороны может фактически сломать ранее корректный код.
Финализаторы выполняются в одном или нескольких системных потоках, о которых приложение не знает. Поэтому наличие финализатора в приложении, которое в остальном однопоточное, само по себе делает его многопоточным. Это создаёт возможность взаимных блокировок и других проблем с потоками.
В конечном счёте финализаторы усиливают связанность в архитектуре приложения. Когда у нескольких компонентов приложения есть финализаторы, финализация объектов одного компонента может задерживать финализацию объектов другого компонента или иным образом мешать ей, особенно потому, что потоки финализаторов общие для всех компонентов. Такое взаимное влияние может привести к тому, что в куче будут накапливаться финализируемые объекты определённого вида, что, как описано выше, вызовет нехватку ресурсов одного компонента, а в итоге — пагубные последствия для других компонентов.
Альтернативные способы
С учётом проблем, связанных с финализацией, разработчикам следует использовать альтернативные способы избежать утечек ресурсов, а именно try-with-resources и cleaners.
-
Try-with-resources — в Java 7 появился оператор
try-with-resources как улучшение показанной выше конструкцииtry-finally. Этот оператор позволяет использовать ресурсы так, что вызов их методовcloseгарантирован независимо от того, возникают ли исключения. Приведённый ранее пример можно переписать так:try (FileInputStream input = new FileInputStream(file1); FileOutputStream output = new FileOutputStream(file2)) { ... copy bytes from input to output ... }try-with-resources правильно обрабатывает все исключительные ситуации, поэтому страховка в виде финализации не нужна. Любой ресурс, который открывается и закрывается в пределах одной лексической области видимости, следует перевести наtry-with-resources. Если экземпляры класса с финализатором могут использоваться исключительно внутри операторовtry-with-resources, финализатор, скорее всего, не нужен и его можно удалить. -
Cleaners — некоторые ресурсы живут слишком долго, чтобы с ними хорошо работал
try-with-resources, поэтому в Java 9 появился API cleaner, помогающий их освобождать. API cleaner позволяет программе зарегистрировать для объекта действие очистки, которое выполняется через некоторое время после того, как объект становится недостижимым. Действия очистки лишены многих недостатков финализаторов:-
Нет воскрешения объектов — действия очистки не имеют доступа к объекту, поэтому воскресить объект невозможно.
-
Включаются по требованию — конструктор может зарегистрировать действие очистки для нового объекта после того, как объект полностью инициализирован. Это значит, что действие очистки никогда не обрабатывает неинициализированный или частично инициализированный объект. Кроме того, программа может отменить действие очистки объекта, и тогда GC больше не нужно планировать это действие.
-
Нет взаимного влияния — разработчик может управлять тем, в каких потоках выполняются действия очистки, и поэтому может предотвратить их взаимное влияние. Кроме того, ошибочный или вредоносный подкласс не может вмешаться в действия очистки, установленные его суперклассом.
Однако, как и финализаторы, действия очистки планирует GC, поэтому они могут страдать от неограниченных задержек. Поэтому API cleaner не следует использовать в ситуациях, когда требуется своевременное освобождение ресурса. Кроме того, cleaners не следует использовать для замены финализатора, который служит лишь страховкой от неперехваченных исключений или пропущенных вызовов методов
close(); в таких случаях, прежде чем переводить финализатор на cleaner, рассмотрите возможность использованияtry-with-resources.Один из сценариев, в которых cleaners, несмотря на свою задержку, ценны для опытных разработчиков, — реализация API, которые не допускают явных методов
close(). Рассмотрим версию классаBigInteger, внутренняя реализация которой использует нативную память. Добавление методаclose()в классBigIntegerв корне изменило бы его модель программирования и исключило бы некоторые оптимизации. Поскольку пользовательский код не может выполнитьcloseдляBigInteger, реализующий вынужден полагаться на GC, который запланирует действие очистки, освобождающее нативную память. Реализующий может соотнести интересы разработчиков, которым выгоден более простой API, с накладными расходами во время выполнения. (Foreign Function & Memory API (JEP 419) в статусе Incubator (инкубационный модуль), который предоставляет лучший способ доступа к нативной памяти, поддерживает использование cleaners для освобождения нативной памяти и предотвращения утечек ресурсов.) -
Аннотация
У финализации есть серьёзные недостатки, которые широко признаются уже несколько десятилетий. Её присутствие в платформе Java обременяет всю экосистему, поскольку подвергает весь код библиотек и приложений рискам для безопасности, надёжности и производительности. Кроме того, она требует постоянных затрат на сопровождение и разработку JDK, особенно реализаций GC. Чтобы платформа Java развивалась дальше, мы объявим финализацию устаревшей и подлежащей удалению.
Описание
Мы предлагаем:
- Добавить параметр командной строки, отключающий финализацию, чтобы GC никогда не планировал выполнение финализаторов, и
- Объявить устаревшими все финализаторы и методы, связанные с финализацией, в стандартном Java API.
Обратите внимание, что финализация отличается и от модификатора final, и от блока finally конструкции try-finally. Никаких изменений ни в final, ни в try-finally не предлагается.
Параметр командной строки для отключения финализации
В JDK 18 финализация по-прежнему включена по умолчанию. Новый параметр командной строки --finalization=disabled отключает финализацию. JVM, запущенная с --finalization=disabled, не будет выполнять никакие финализаторы — даже объявленные в самом JDK.
С помощью этого параметра можно определить, зависит ли ваше приложение от финализации, и проверить, как оно будет вести себя после удаления финализации. Например, можно сначала запустить нагрузочный тест приложения без этого параметра, чтобы финализация была включена, и записать такие метрики, как:
- Профили памяти для кучи Java и/или нативной памяти,
- Статистика из
BufferPoolMXBeanиUnixOperatingSystemMXBean::getOpenFileDescriptorCount, а также - Событие
jdk.FinalizerStatisticsиз JDK Flight Recorder (JFR). Это событие предоставляет данные об использовании финализаторов во время выполнения, как описано в примечаниях к выпуску JDK 18. JFR можно использовать так:
java -XX:StartFlightRecording:filename=recording.jfr ...
jfr print --events jdk.FinalizerStatistics recording.jfr
Затем можно повторно запустить нагрузочный тест с этим параметром, чтобы финализация была отключена. Значительное ухудшение полученных метрик, ошибка или сбой укажут на необходимость выяснить, где приложение зависит от финализации. В целом схожие результаты двух запусков дадут некоторую уверенность в том, что последующее удаление финализации не затронет приложение.
Отключение финализации может иметь непредсказуемые последствия, поэтому этот параметр следует использовать только для тестирования, а не в рабочей среде.
Если финализация отключена, JFR не будет генерировать события jdk.FinalizerStatistics. Кроме того, jcmd GC.finalizer_info сообщит, что финализация отключена (вместо того чтобы сообщать количество объектов, ожидающих финализации).
Для полноты поддерживается и --finalization=enabled.
Объявление финализаторов устаревшими в стандартном Java API
Мы окончательно объявим устаревшими следующие методы в модулях java.base и java.desktop, пометив их аннотацией @Deprecated(forRemoval=true):
java.lang.Object.finalize()java.lang.Enum.finalize()java.awt.Graphics.finalize()java.awt.PrintJob.finalize()java.util.concurrent.ThreadPoolExecutor.finalize()javax.imageio.spi.ServiceRegistry.finalize()javax.imageio.stream.FileCacheImageInputStream.finalize()javax.imageio.stream.FileImageInputStream.finalize()javax.imageio.stream.FileImageOutputStream.finalize()javax.imageio.stream.ImageInputStreamImpl.finalize()javax.imageio.stream.MemoryCacheImageInputStream.finalize()
(Три других финализатора в java.awt.** уже были окончательно объявлены устаревшими и были удалены из Java 18 независимо от этого JEP.)
Кроме того, мы:
-
Окончательно объявим устаревшими
java.lang.Runtime.runFinalization()иjava.lang.System.runFinalization(). Без финализации эти методы не имеют смысла. -
Объявим устаревшим метод
getObjectPendingFinalizationCount()в интерфейсеjava.lang.management.MemoryMXBeanмодуляjava.management, пометив его аннотацией@Deprecated(forRemoval=false).Этот метод не является частью механизма финализации, а лишь запрашивает сведения о его работе. Мы объявим этот метод устаревшим, потому что разработчикам следует избегать его использования: после удаления финализации ни один объект никогда не будет ожидать финализации, и метод всегда будет возвращать ноль. Мы не будем объявлять метод окончательно устаревшим, потому что не планируем его удалять.
MemoryMXBean— это интерфейс, поэтому удаление метода может негативно сказаться на множестве независимых реализаций.
Дальнейшая работа
Мы ожидаем длительного переходного периода перед удалением финализации. За это время разработчики смогут оценить, зависят ли их системы от финализации, и при необходимости перенести свой код. Мы также предполагаем несколько других шагов:
-
Сам JDK активно использует финализаторы. Часть из них уже удалена или переведена на
Cleaner. Удаление оставшихся финализаторов отслеживается в JDK-8253568. -
Финализаторы используются в известных библиотеках, таких как Netty, Log4j, Guava и Apache Commons. Мы будем работать с их сопровождающими, чтобы эти библиотеки могли безопасно отказаться от финализации.
-
Мы опубликуем документацию, которая поможет разработчикам в различных аспектах отказа от финализации, например:
- Как найти финализаторы в коде библиотек и приложений (например, с помощью
jdeprscan), - Как определить, зависит ли система от финализации, и
- Как перевести код с финализаторами на
try-with-resources или cleaners.
- Как найти финализаторы в коде библиотек и приложений (например, с помощью
-
Будущие версии JDK могут включать некоторые или все следующие изменения:
- Выдавать предупреждение во время выполнения, если используются финализаторы,
- Отключить финализацию по умолчанию и требовать параметр
--finalization=enabledдля её повторного включения, - Позднее удалить механизм финализации, оставив на месте неработающие API, и,
- Наконец, после того как большая часть кода будет перенесена, удалить перечисленные выше окончательно устаревшие методы, включая
Object::finalize.
Мы не планируем пересматривать исторически различающиеся роли WeakReference и PhantomReference. В то же время мы ожидаем, что при удалении финализации обновим Java Language Specification, поскольку финализаторы взаимодействуют с моделью памяти Java (Java Memory Model).