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