Сборка мусора в 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 помещает их в «очередь ссылок».
Логика, определяющая то, как они собираются, всегда связана с концепцией достижимости (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), который отслеживает зарегистрированные объекты. Как только объект становится недоступным, задача очистки для этого объекта ставится в очередь на выполнение в этом фоновом потоке.
В следующем фрагменте используется 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/
Добавить комментарий