«1. Обзор

В этом руководстве мы сосредоточимся на ключевом аспекте языка Java — методе finalize, предоставляемом корневым классом Object.

Проще говоря, это вызывается перед сборкой мусора для конкретного объекта.

2. Использование финализаторов

Метод finalize() называется финализатором.

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

Основная цель финализатора, однако, состоит в освобождении ресурсов, используемых объектами, прежде чем они будут удалены из памяти. Финализатор может работать как основной механизм для операций очистки или как подстраховка, когда другие методы не работают.

Чтобы понять, как работает финализатор, давайте взглянем на объявление класса:

public class Finalizable {
    private BufferedReader reader;

    public Finalizable() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        this.reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    // other class members
}

Класс Finalizable имеет средство чтения полей, которое ссылается на закрываемый ресурс. Когда объект создается из этого класса, он создает новый экземпляр BufferedReader, читающий из файла в пути к классам.

Такой экземпляр используется в методе readFirstLine для извлечения первой строки в заданном файле. Обратите внимание, что в данном коде средство чтения не закрыто.

Мы можем сделать это с помощью финализатора:

@Override
public void finalize() {
    try {
        reader.close();
        System.out.println("Closed BufferedReader in the finalizer");
    } catch (IOException e) {
        // ...
    }
}

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

На самом деле время, когда сборщик мусора вызывает финализаторы, зависит от реализации JVM и состояния системы, которые мы не можем контролировать.

Чтобы сборка мусора происходила на месте, воспользуемся методом System.gc. В реальных системах мы никогда не должны вызывать это явно по ряду причин:

  1. It’s costly
  2. It doesn’t trigger the garbage collection immediately – it’s just a hint for the JVM to start GC
  3. JVM knows better when GC needs to be called

Если нам нужно форсировать GC, мы можем использовать для этого jconsole.

Ниже приведен тестовый пример, демонстрирующий работу финализатора:

@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
    String firstLine = new Finalizable().readFirstLine();
    assertEquals("baeldung.com", firstLine);
    System.gc();
}

В первом операторе создается объект Finalizable, затем вызывается его метод readFirstLine. Этот объект не назначен какой-либо переменной, поэтому он подходит для сборки мусора при вызове метода System.gc.

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

Когда мы запустим предоставленный тест, в консоли будет напечатано сообщение о закрытии буферизованного считывателя в финализаторе. Это означает, что был вызван метод finalize, и он очистил ресурс.

До этого момента финализаторы выглядели как отличный способ для операций перед уничтожением. Однако это не совсем так.

В следующем разделе мы увидим, почему их следует избегать.

3. Избегайте финализаторов

Несмотря на преимущества, которые они приносят, у финализаторов есть много недостатков.

3.1. Недостатки финализаторов

Давайте рассмотрим несколько проблем, с которыми мы столкнемся при использовании финализаторов для выполнения критических действий.

Первая заметная проблема — отсутствие оперативности. Мы не можем знать, когда запускается финализатор, так как сборка мусора может произойти в любое время.

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

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

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

Последняя проблема, о которой мы поговорим, это отсутствие обработки исключений во время финализации. Если финализатор выдает исключение, процесс финализации останавливается, оставляя объект в поврежденном состоянии без какого-либо уведомления.

3.2. Демонстрация эффектов финализаторов

«Пришло время отложить теорию в сторону и посмотреть на эффект финализаторов на практике.

Давайте определим новый класс с непустым финализатором:

public class CrashedFinalizable {
    public static void main(String[] args) throws ReflectiveOperationException {
        for (int i = 0; ; i++) {
            new CrashedFinalizable();
            // other code
        }
    }

    @Override
    protected void finalize() {
        System.out.print("");
    }
}

Обратите внимание на метод finalize() — он просто выводит на консоль пустую строку. Если бы этот метод был полностью пустым, JVM обработала бы объект так, как если бы у него не было финализатора. Поэтому нам нужно обеспечить finalize() реализацией, которая в данном случае почти ничего не делает.

Внутри основного метода новый экземпляр CrashedFinalizable создается на каждой итерации цикла for. Этот экземпляр не назначен какой-либо переменной, поэтому подходит для сборки мусора.

Давайте добавим несколько операторов в строку, отмеченную // другим кодом, чтобы увидеть, сколько объектов существует в памяти во время выполнения:

if ((i % 1_000_000) == 0) {
    Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
    Field queueStaticField = finalizerClass.getDeclaredField("queue");
    queueStaticField.setAccessible(true);
    ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queueStaticField.get(null);

    Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength");
    queueLengthField.setAccessible(true);
    long queueLength = (long) queueLengthField.get(referenceQueue);
    System.out.format("There are %d references in the queue%n", queueLength);
}

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

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

...
There are 21914844 references in the queue
There are 22858923 references in the queue
There are 24202629 references in the queue
There are 24621725 references in the queue
There are 25410983 references in the queue
There are 26231621 references in the queue
There are 26975913 references in the queue
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.lang.ref.Finalizer.register(Finalizer.java:91)
    at java.lang.Object.<init>(Object.java:37)
    at com.baeldung.finalize.CrashedFinalizable.<init>(CrashedFinalizable.java:6)
    at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9)

Process finished with exit code 1

Похоже, сборщик мусора плохо справлялся со своей задачей — количество объектов продолжало увеличиваться, пока не произошел сбой системы.

Если бы мы удалили финализатор, количество ссылок обычно было бы равно 0, и программа продолжала бы работать вечно.

3.3. Объяснение

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

При создании объекта, также называемого референтом, который имеет финализатор, JVM создает сопровождающий эталонный объект типа java.lang.ref.Finalizer. После того, как референт готов к сборке мусора, JVM помечает эталонный объект как готовый к обработке и помещает его в очередь ссылок.

Мы можем получить доступ к этой очереди через очередь статических полей в классе java.lang.ref.Finalizer.

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

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

Если поток продолжает создавать объекты с высокой скоростью, как это произошло в нашем примере, поток Finalizer не может угнаться за ним. В конце концов, память не сможет хранить все объекты, и мы получим ошибку OutOfMemoryError.

Обратите внимание, что ситуация, когда объекты создаются со скоростью деформации, как показано в этом разделе, не часто встречается в реальной жизни. Однако он демонстрирует важный момент — финализаторы очень дороги.

4. Пример без финализатора

Давайте рассмотрим решение, обеспечивающее ту же функциональность, но без использования метода finalize(). Обратите внимание, что приведенный ниже пример — не единственный способ заменить финализаторы.

Вместо этого он используется для демонстрации важного момента: всегда есть варианты, которые помогают нам избежать финализаторов.

Вот объявление нашего нового класса:

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // handle exception
        }
    }
}

Нетрудно заметить, что единственная разница между новым классом CloseableResource и нашим предыдущим классом Finalizable заключается в реализации интерфейса AutoCloseable вместо определения финализатора.

Обратите внимание, что тело метода закрытия CloseableResource почти такое же, как тело финализатора в классе Finalizable.

Ниже приведен тестовый метод, который читает входной файл и освобождает ресурс после завершения своей работы:

@Test
public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
    try (CloseableResource resource = new CloseableResource()) {
        String firstLine = resource.readFirstLine();
        assertEquals("baeldung.com", firstLine);
    }
}

В приведенном выше тесте экземпляр CloseableResource создается в блоке try функции try-with-. resources, поэтому этот ресурс автоматически закрывается, когда блок try-with-resources завершает выполнение.

Запустив данный тестовый метод, мы увидим сообщение, распечатываемое из метода close класса CloseableResource.

5. Заключение

«В этом руководстве мы сосредоточились на основной концепции Java — методе finalize. Это выглядит полезным на бумаге, но может иметь уродливые побочные эффекты во время выполнения. И, что более важно, всегда есть альтернативное решение для использования финализатора.

Один важный момент, на который стоит обратить внимание, это то, что finalize устарела, начиная с Java 9, и в конечном итоге будет удалена.

Как всегда, исходный код этого руководства можно найти на GitHub.