Сборка мусора в 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.
 
           
 
DanilPomortsrv
Спасибо за статью! Кажется что случай утечки памяти в Java не такой частый, какие у кого были не очевидные ситуации когда память утекала?