Сборка мусора в 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.

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

Комментарии (1)


  1. DanilPomortsrv
    22.05.2025 16:38

    Спасибо за статью! Кажется что случай утечки памяти в Java не такой частый, какие у кого были не очевидные ситуации когда память утекала?