Очистите свою память: от Finalize к Cleaner

от автора

Сборка мусора в Java отвечает за управление памятью, но не очищает ресурсы, не связанные с памятью, такие как сокеты или дескрипторы файлов.

Без надлежащего управления могут возникнуть утечки ресурсов, что приведет к снижению производительности или сбоям.

Java  Cleaner API, представленный в Java 9, обеспечивает современный и эффективный механизм очистки ресурсов, когда объекты больше не доступны.

Он устраняет недостатки устаревшего метода finalize(), предлагая предсказуемый и эффективный способ управления ресурсами, не связанными с памятью: поэтому давайте совершим небольшой экскурс по методам очистки памяти от finalize до Cleaner API.

Почему метод finalize() устарел/удалён?

Метод finalize()  был первоначально введен в Java, чтобы предоставить объектам возможность выполнять действия по очистке перед сборкой мусора. Он принадлежит подклассам java.lang.Object и может быть переопределен ими для освобождения ресурсов, таких как дескрипторы файлов или сетевые сокеты. Почему этот подход оказался проблематичным?

  • Непредсказуемость выполнения: метод finalize() вызывается в неопределенное время, когда сборщик мусора решает уничтожить объект.

  • Снижение производительности : уничтожение объектов с помощью метода finalize() занимает больше времени, поскольку они должны пройти дополнительный цикл сборки мусора.

  • Утечки памяти: если объект непреднамеренно сохранен (например, из-за исключения в finalize()), он может никогда не быть удален сборщиком мусора.

  • Механизм очереди финализации: выполнение finalize() происходит в отдельном потоке (поток Finalizer), что может привести к конфликтам потоков и задержкам.

Как Cleaner связан со ссылочными классами Java?

В Java существует четыре типа ссылок, различающихся по способу сбора мусора:

  • Сильные ссылки: объекты, имеющие активную сильную ссылку, не подлежат сборке мусора. Объект подвергается сборке мусора только тогда, когда переменная, на которую была сделана строгая ссылка, указывает на null.

  • Слабые ссылки: объекты, на которые ссылается слабая ссылка, не препятствуют тому, чтобы их референты были финализируемыми и восстановленными.

  • Мягкие ссылки: объекты, на которые ссылаются с помощью мягкой ссылки. Даже если объект свободен для сборки мусора, он не будет собран до тех пор, пока JVM не понадобится память.

  • Фантомные ссылки: объекты, на которые ссылаются фантомные ссылки, подлежат сборке мусора, но перед их удалением JVM помещает их в «очередь ссылок».

Очистите свою память: от финализации к очистке - Java Reference Hierarchy

Java Reference Hierarchy

Логика, определяющая то, как они собираются, всегда связана с концепцией достижимости (reachability), как описано в соответствующем Javadoc. Классы ссылок Java не так просты в использовании, и получаемый код иногда сложен.
Мы также должны учитывать, что в случае фантомной ссылки, после регистрации референта, ссылка всегда возвращает null, что кажется делает ее бесполезной, но это не так!

Вот пример фантомной ссылки (PhantomReference):

import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.util.HashMap; import java.util.Map;  public class UsingPhantomRef {      static class Resource {         public void cleaning() {             System.out.println("cleaning");         }     }      record ResourceHolder(Resource resource) {}      private static final Map lookup = new HashMap();      private static final ReferenceQueue queue = new ReferenceQueue();      public static void main(String[] args) {          var holder = new ResourceHolder(new Resource());         lookup.put(new PhantomReference(holder, queue), holder.resource());          holder = null;         System.gc();          Reference element = null;         while ((element = queue.poll()) == null) {             System.out.println("wating for GC");         }         System.out.println("GCollected!");         lookup.remove(element).cleaning();     } }

Cleaner имеет общие черты с классами ссылок Java, но он более эффективен для управления внешними ресурсами. В то время как PhantomReference обеспечивает логику очистки, Cleaner добавляет слой абстракции, что упрощает его реализацию и управление. Внутри Cleaner использует фантомную ссылку за кулисами, чтобы обнаружить, когда объект становится недоступным.

Вот пример использования Cleaner:

import java.lang.ref.Cleaner;  public class BasicCleanerExample {      private static final Cleaner cleaner = Cleaner.create();      static class CleaningAction implements Runnable {         @Override         public void run() {             System.out.println("Resource cleaned up!");         }     }      static class ManagedObject {         private final Cleaner.Cleanable cleanable;          ManagedObject() {             cleanable = cleaner.register(this, new CleaningAction());         }     }      public static void main(String[] args) {         new ManagedObject();         System.gc();         pause();     } }

За кулисами Cleaner

Реализация Cleaner нетривиальна. Она использует комбинацию Java PhantomReferenceи фонового потока демона. Давайте рассмотрим его внутреннюю работу:

  • Регистрация в Cleaner: объект, зарегистрированный в Cleaner, ассоциируется с задачей Cleanable. За объектом, по сути, «наблюдает» фоновый поток демона.

  • Управление фантомными ссылками: Под капотом Cleaner используется PhantomReference для отслеживания достижимости объекта.

  • Поток демона для очистки: фреймворк Cleaner использует выделенный поток демона (обычно называемый Common-Cleaner), который отслеживает зарегистрированные объекты. Как только объект становится недоступным, задача очистки для этого объекта ставится в очередь на выполнение в этом фоновом потоке.

Очистите свою память: от финализации к очистке — поток JVM и поток Common-Cleaner

Поток JVM и поток Common-Cleaner

В следующем фрагменте используется Cleaner для управления файлами.

import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.lang.ref.Cleaner;  public class FileCleanerExample {      private static final Cleaner cleaner = Cleaner.create();      static class FileResource implements Runnable {          private final File file;          FileResource(File file) {             this.file = file;             System.out.printf("resource create with temporary file: %s%n", file.getAbsolutePath());         }          @Override         public void run() {             if (file.exists()) {                 System.out.printf("temporary file deleted: %s%n",file.delete());             }         }     }      static class ManagedFile {         private Cleaner.Cleanable cleanable;          ManagedFile(String path) throws IOException {             var file = new File(path);             cleanable = cleaner.register(this, new FileResource(file));             new FileWriter(file).write("Temporary data");         }     }      public static void main(String[] args) throws IOException {         var managed = new ManagedFile("temp.txt");         managed = null;         System.gc();         pause();     }     // ... }

Сравнение Cleaner c try-with-resources

Пример с файлом может навести на мысль о более идиоматичном решении с использованием конструкции try-with-resources. Это может быть реальным решением, но в меньшей степени, если мы рассматриваем конкретные ресурсы, такие как изображения или буферы памяти, напрямую сопоставленные с памятью. В качестве последнего замечания, нам также нужно учитывать, что Autocloseable может использоваться с Cleanable, вызывая метод clean() из метода close(),что имеет тот же эффект, что и операция GC.
Вот соответствующий пример.

import java.lang.ref.Cleaner;  public class CleanerWithCloseExample {      private static final Cleaner cleaner = Cleaner.create();      static class CleaningAction implements Runnable {         @Override         public void run() {             System.out.println("Resource cleaned up!");         }     }      static class ManagedObject implements AutoCloseable{         private final Cleaner.Cleanable cleanable;          ManagedObject() {             cleanable = cleaner.register(this, new CleaningAction());         }          @Override         public void close(){             System.out.println("close invoked!");             cleanable.clean();         }     }      public static void main(String[] args) {         var object = new ManagedObject();         try(object){             System.out.printf("using: %s%n",object);         }     } }

Следующая таблица позволяет нам обобщить представленные инструменты и выбрать тот, который лучше всего подходит для наших целей.

Характеристика

try-with-resources

Cleaner

Тип ресурса

Автоматически закрываемый

Он также работает без объекта Autocloaseable.

Время очистки

Немедленная

Отложенная, асинхронная

Сценарий использования

Когда требуется точное время очистки

При работе с внешними ресурсами или объектами без явных методов закрытия

Накладные расходы

Минимальные

Больше из-за фонового потока

Избегайте чрезмерного использования Cleaner.

Использование Cleaner более простое, чем использование ссылок. Тем не менее мы должны проявлять осторожность при использовании такого рода ресурсов: используйте только тогда Cleaner, когда среда выполнения не может освободить ресурсы с помощью try-with-resourcesили явных вызовов close().
Также следует учитывать, что мы создаем новые элементы, и иногда лучше использовать один Cleaner для нескольких ресурсов или для прослушивания действия очистки.
Другие аспекты также требуют нашего внимания, например, действия по очистке должны выполняться в соответствии с некоторыми правилами:

  • Избегайте использования лямбда выражений, поскольку это может привести к захвату ссылки на объект путем ссылки на поля очищаемого объекта, что сделает объект фантомно достижимым.

  • Действия по очистке могут вызываться одновременно с другими действиями по очистке. Они должны выполняться быстро и не блокироваться: это может задержать обработку других действий по очистке, зарегистрированных для того же очистителя.

  • Мы также можем рассмотреть возможность выполнения действия по очистке и делегирования его другому пулу потоков, но эта идея может привести к увеличению сложности и не решить проблемы параллелизма.

Заключение

Java Cleaner API обеспечивает современный, эффективный подход к управлению ресурсами, устраняя ограничения метода finalize(). Используя Cleaner, разработчики могут гарантировать, что неавтозакрываемые ресурсы, такие как нативная память и кэш, будут надлежащим образом очищены, когда они больше не нужны. Однако для детерминированного управления ресурсами всегда предпочтительнее использование конструкции try-with-resources, когда это возможно.

Понимая, когда и как использовать Cleaner, разработчики могут писать более надежные, высокопроизводительные и эффективные с точки зрения памяти приложения Java. Освоение этих лучших практик будет необходимо для создания надежного и масштабируемого программного обеспечения по мере развития Java.

Дополнительные ресурсы


ссылка на оригинал статьи https://habr.com/ru/articles/911046/