После того, как устаревшая финализация Java была объявлена устаревшей( JEP-421 ) в Java 18, мы должны использовать любой из двух доступных подходов для очистки ресурсов:
- утверждения try-with-resources
- Более чистый API
В этом уроке мы изучим основы API Cleaner и его использование.
1. Потребность в уборщиках
1.1 Проблемы с финализаторами
Как подробно обсуждалось в разделе «Почему не следует использовать finalize()», этот метод вызывается сборщиком мусора, когда в JVM не осталось ни сильных, ни слабых ссылок на этот объект.
В этот момент среда выполнения Java выполнит метод Object.finalize() для таких потерянных объектов, а затем специфичный для приложения код очистит все ресурсы, такие как потоки ввода-вывода или дескрипторы хранилищ данных.
Хотя реализация кажется простой, она может никогда не запуститься или запуститься с большой задержкой, поскольку GC обычно работает только тогда, когда это необходимо для удовлетворения запросов на выделение памяти. Поэтому мы зависим от GC, чтобы вызвать ее, что крайне непредсказуемо.
Другая проблема заключается в том, что финализаторы могут работать в любом потоке, создавая условия ошибок, которые очень трудно отладить, как и любую другую проблему параллелизма. Если такие проблемы возникают в производстве, их трудно воспроизвести и отладить.
1.2. Какую помощь оказывают уборщики?
Очистители, представленные в Java 9, позволяют нам определять действия по очистке для групп объектов. Очистители реализованы с помощью интерфейса Cleanable, который происходит от Runnable.
Каждый Cleanable представляет объект и действие очистки, зарегистрированное в Cleaner. Каждый Cleanable выполняется в выделенном потоке. Все исключения, выдаваемые действием очистки, игнорируются.
Наиболее эффективным использованием является явный вызов метода clean(), когда объект закрыт или больше не нужен.
Обратите внимание, что действие очистки не должно ссылаться на регистрируемый объект. В этом случае объект не станет фантомно достижимым, а действие очистки не будет вызвано автоматически.
По этой причине не используйте внутренние классы для реализации действий очистки, поскольку внутренний класс неявно содержит ссылку на внешний объект, что не позволяет сборщику мусора удалить его.
Поведение очистителей во время System.exit зависит от реализации. Не дается никаких гарантий относительно того, будут ли вызываться действия по очистке или нет.
2. Руководство по внедрению
Oracle предоставляет пример справки по реализации в документации:
открытый класс CleaningExample реализует AutoCloseable {// Очиститель, желательно общий для библиотекиprivate static final Очиститель очиститель = ;Статический класс State реализует Runnable {Состояние(...) {// инициализируем состояние, необходимое для действия очистки}public void run() {// действие очистки, обращающееся к состоянию, выполняется не более одного раза}}частное конечное государство;частный финальный очиститель.Очищаемый очищаемый;public CleaningExample() {это.состояние = новое Состояние(...);этот.очищаемый = очиститель.регистр(это, состояние);}публичная пустота закрыть() {очищаемый.clean();}}
- Обратите внимание на метод close(). Мы можем вызвать этот метод явно в коде приложения, чтобы запустить процесс очистки ресурсов.
- JVM автоматически вызовет метод close(), если разработчик явно не вызвал его.
Класс CleaningExample реализует интерфейс AutoCloseable, поэтому мы также можем использовать этот класс внутри оператора try-with-resources.
Каждый Cleaner порождает поток, поэтому рекомендуется создавать только один Cleaner для всего приложения или библиотеки. Помните, что создавать новые потоки дорого.
3. Как применять очистители
Объект Cleaner может быть создан с помощью его статического метода create, как показано ниже. Этот метод создает экземпляр Cleaner и запускает поток-демон, который продолжает отслеживать объекты, подлежащие сборке мусора.
Cleaner cleaner = Cleaner.create();
Далее нам нужно зарегистрировать объект и действие очистки с помощью метода register(). Этот метод принимает два аргумента:
- Объект, за которым следит уборщик для сбора мусора.
- Экземпляр java.lang.Runnable, представляющий действие очистки, которое необходимо выполнить.
cleaner.register(object, runnable);
Наконец, мы можем либо вызвать метод clean() самостоятельно, либо дождаться, пока GC вызовет его. Метод clean() отменяет регистрацию runnable и вызывает действие очистки.
runnable.clean();
4. Демонстрация
В этой демонстрации мы создаем симуляцию ресурса с классом Resource, который в этом примере ничего не делает.
Ресурс публичного класса {//Демонстрационный ресурс}
Кроме того, мы обсудили, что в каждом приложении должен быть только один экземпляр Cleaner из-за накладных расходов на создание потоков, поэтому мы создаем Cleaner в служебном классе.
импорт java.lang.ref.Cleaner;открытый класс AppCleanerProvider {частный статический финальный очиститель CLEANER = Cleaner.create();публичный статический очиститель getCleaner() {вернуть ОЧИСТИТЕЛЬ;}}
Теперь нам нужно написать класс, который будет иметь доступ к Ресурсу, а также к Очистителю. Мы хотим, чтобы при сборке мусора ClassAccessingResource вызывался метод cleanResourceAction() для освобождения Ресурса.
ClassAccessingResource реализует интерфейс AutoClosable также для обеспечения совместимости с операторами try-with-resources. Это необязательно. Мы можем написать метод close() и вызвать его самостоятельно.
импорт java.lang.ref.Cleaner;открытый класс ClassAccessingResource реализует AutoCloseable {private final Очиститель очиститель = AppCleanerProvider.getCleaner();частный финальный очиститель.Очищаемый очищаемый;//Этот ресурс необходимо очистить после использования.частный конечный ресурсный ресурс;publicClassAccessingResource() {этот.ресурс = новый Ресурс();этот.cleanable = очиститель.register(это, cleanResourceAction(ресурс));}public void businessOperation() {//Доступ к ресурсу в методахSystem.out.println("Внутри businessOperation()");}public void anotherBusinessOperation() {//Доступ к ресурсу в методахSystem.out.println("Внутри другойБизнесОперации()");}@Переопределитьpublic void close() выдает исключение {очищаемый.clean();}частный статический Runnable cleanResourceAction(конечный ресурс ресурса) {возврат() -> {// Выполнить действия по очистке// ресурс.release();System.out.println("Ресурс очищен!!");};}}
Чтобы продемонстрировать очистку ресурсов, я создал экземпляры ClassAccessingResource и вызвал очиститель двумя способами: явно и неявно.
public class CleanerExample {public static void main(final String[] args) throws Exception {//1 Implicit Cleanuptry(final ClassAccessingResource clazzInstance= new ClassAccessingResource()) {// Safely use the resourceclazzInstance.businessOperation();clazzInstance.anotherBusinessOperation();}//2 Explicit Cleanupfinal ClassAccessingResource clazzInstance = new ClassAccessingResource();clazzInstance.businessOperation();clazzInstance.anotherBusinessOperation();clazzInstance.close();}}
Обратите внимание на вывод. В обоих случаях очистка ресурсов запускается один раз.
Внутри businessOperation()Внутри anotherBusinessOperation()Ресурс очищен!!Внутри businessOperation()Внутри anotherBusinessOperation()Ресурс очищен!!
5. Заключение
Хотя очистители не так просты, как метод finalize(), они обеспечивают больший контроль над временем, когда JVM запускает очистку ресурсов. Они не полностью зависят от сборки мусора и могут быть запущены разработчиками.
Двигаясь дальше, нам необходимо удалить все методы finalize() из исходного кода приложения и начать использовать Cleaners, как рекомендует Oracle.