Руководство по методу финализации в Java

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. Это дорого
  2. Сборка мусора запускается не сразу - это просто подсказка JVM для запуска сборки мусора.
  3. JVM лучше знает, когда нужно вызвать 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 () реализацию, которая в данном случае почти ничего не делает.

Внутри основного метода на каждой итерации цикла 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); }

The given statements access some fields in internal JVM classes and print out the number of object references after every million iterations.

Let's start the program by executing the main method. We may expect it to run indefinitely, but that's not the case. After a few minutes, we should see the system crash with an error similar to this:

... 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

Looks like the garbage collector didn't do its job well – the number of objects kept increasing until the system crashed.

If we removed the finalizer, the number of references would usually be 0 and the program would keep running forever.

3.3. Explanation

To understand why the garbage collector didn't discard objects as it should, we need to look at how the JVM works internally.

When creating an object, also called a referent, that has a finalizer, the JVM creates an accompanying reference object of type java.lang.ref.Finalizer. After the referent is ready for garbage collection, the JVM marks the reference object as ready for processing and puts it into a reference queue.

We can access this queue via the static field queue in the java.lang.ref.Finalizer class.

Meanwhile, a special daemon thread called Finalizer keeps running and looks for objects in the reference queue. When it finds one, it removes the reference object from the queue and calls the finalizer on the referent.

During the next garbage collection cycle, the referent will be discarded – when it's no longer referenced from a reference object.

If a thread keeps producing objects at a high speed, which is what happened in our example, the Finalizer thread cannot keep up. Eventually, the memory won't be able to store all the objects, and we end up with an OutOfMemoryError.

Notice a situation where objects are created at warp speed as shown in this section doesn't often happen in real life. However, it demonstrates an important point – finalizers are very expensive.

4. No-Finalizer Example

Let's explore a solution providing the same functionality but without the use of finalize() method. Notice that the example below isn't the only way to replace finalizers.

Instead, it's used to demonstrate an important point: there are always options that help us to avoid finalizers.

Here's the declaration of our new class:

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 } } }

It's not hard to see that the only difference between the new CloseableResource class and our previous Finalizable class is the implementation of the AutoCloseable interface instead of a finalizer definition.

Notice that the body of the close method of CloseableResource is almost the same as the body of the finalizer in class Finalizable.

The following is a test method, which reads an input file and releases the resource after finishing its job:

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

In the above test, a CloseableResource instance is created in the try block of a try-with-resources statement, hence that resource is automatically closed when the try-with-resources block completes execution.

Running the given test method, we'll see a message printed out from the close method of the CloseableResource class.

5. Conclusion

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

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

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