Рубрики
Без рубрики

Руководство по методу finalize в Java

Узнайте о методе finalize Java и о том, почему его не следует использовать.

Автор оригинала: baeldung.

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 имеет поле reader , которое ссылается на закрываемый ресурс. Когда объект создается из этого класса, он создает новый BufferedReader экземпляр, считывающий из файла в пути к классу.

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

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

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

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

На самом деле время, в течение которого сборщик мусора вызывает finalize, зависит от реализации JVM и условий системы, которые находятся вне нашего контроля.

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

  1. Это дорого стоит
  2. Это не сразу запускает сборку мусора – это просто подсказка для JVM, чтобы запустить GC
  3. JVM лучше знает, когда нужно вызвать GC

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

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

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

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

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

Когда мы запустим предоставленный тест, на консоли будет напечатано сообщение о закрытии bufferedreader в финализаторе. Это означает, что был вызван метод 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() реализацию, которая в данном случае почти ничего не делает.

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

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

if ((i % 1_000_000) == 0) {
    Class finalizerClass = Class.forName("java.lang.ref.Finalizer");
    Field queueStaticField = finalizerClass.getDeclaredField("queue");
    queueStaticField.setAccessible(true);
    ReferenceQueue 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 и выводят количество ссылок на объекты после каждого миллиона итераций.

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

...
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.(Object.java:37)
    at com.baeldung.finalize.CrashedFinalizable.(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 помечает ссылочный объект как готовый к обработке и помещает его в очередь ссылок.

Мы можем получить доступ к этой очереди через статическое поле queue в классе java.lang.ref.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
        }
    }
}

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

Обратите внимание, что тело close метода Closeable Resource почти совпадает с телом финализатора в классе Finalizable .

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

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

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

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

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

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

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

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