Eine Anleitung zur Finalisierungsmethode in Java

1. Übersicht

In diesem Tutorial konzentrieren wir uns auf einen Kernaspekt der Java-Sprache - die Finalisierungsmethode , die von der Root- Object- Klasse bereitgestellt wird .

Einfach ausgedrückt wird dies vor der Speicherbereinigung für ein bestimmtes Objekt aufgerufen.

2. Finalizer verwenden

Die Methode finalize () wird als Finalizer bezeichnet.

Finalizer werden aufgerufen, wenn JVM herausfindet, dass diese bestimmte Instanz durch Müll gesammelt werden sollte. Ein solcher Finalizer kann alle Operationen ausführen, einschließlich der Wiederbelebung des Objekts.

Der Hauptzweck eines Finalizers besteht jedoch darin, Ressourcen freizugeben, die von Objekten verwendet werden, bevor sie aus dem Speicher entfernt werden. Ein Finalizer kann als primärer Mechanismus für Bereinigungsvorgänge oder als Sicherheitsnetz fungieren, wenn andere Methoden fehlschlagen.

Schauen wir uns eine Klassendeklaration an, um zu verstehen, wie ein Finalizer funktioniert:

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 }

Die Klasse Finalizable verfügt über einen Feldleser , der auf eine verschließbare Ressource verweist. Wenn ein Objekt aus dieser Klasse erstellt wird, erstellt es eine neue BufferedReader- Instanz, die aus einer Datei im Klassenpfad liest.

Eine solche Instanz wird in der readFirstLine- Methode verwendet, um die erste Zeile in der angegebenen Datei zu extrahieren. Beachten Sie, dass der Reader im angegebenen Code nicht geschlossen ist.

Wir können das mit einem Finalizer machen:

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

Es ist leicht zu erkennen, dass ein Finalizer wie jede normale Instanzmethode deklariert wird.

In Wirklichkeit hängt der Zeitpunkt, zu dem der Garbage Collector Finalizer aufruft, von der Implementierung der JVM und den Systembedingungen ab, die außerhalb unserer Kontrolle liegen.

Um die Speicherbereinigung vor Ort zu ermöglichen, nutzen wir die System.gc- Methode. In realen Systemen sollten wir dies aus einer Reihe von Gründen niemals explizit aufrufen:

  1. Es ist teuer
  2. Es löst die Garbage Collection nicht sofort aus - es ist nur ein Hinweis für die JVM, GC zu starten
  3. JVM weiß besser, wann GC aufgerufen werden muss

Wenn wir GC erzwingen müssen, können wir dafür jconsole verwenden .

Das Folgende ist ein Testfall, der die Funktionsweise eines Finalizers demonstriert:

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

In der ersten Anweisung wird ein finalisierbares Objekt erstellt und anschließend die Methode readFirstLine aufgerufen. Dieses Objekt ist keiner Variablen zugewiesen und kann daher beim Aufrufen der System.gc- Methode zur Speicherbereinigung verwendet werden.

Die Zusicherung im Test überprüft den Inhalt der Eingabedatei und wird nur verwendet, um zu beweisen, dass unsere benutzerdefinierte Klasse wie erwartet funktioniert.

Wenn wir den bereitgestellten Test ausführen, wird auf der Konsole eine Meldung gedruckt, dass der gepufferte Reader im Finalizer geschlossen wird. Dies bedeutet, dass die Finalize- Methode aufgerufen wurde und die Ressource bereinigt wurde.

Bis zu diesem Punkt scheinen Finalizer eine großartige Möglichkeit für Vorzerstörungsoperationen zu sein. Das stimmt jedoch nicht ganz.

Im nächsten Abschnitt werden wir sehen, warum ihre Verwendung vermieden werden sollte.

3. Vermeiden von Finalisierern

Trotz der Vorteile, die sie mit sich bringen, haben Finalizer viele Nachteile.

3.1. Nachteile von Finalisierern

Schauen wir uns einige Probleme an, mit denen wir konfrontiert werden, wenn wir Finalizer verwenden, um kritische Aktionen auszuführen.

Das erste auffällige Problem ist die mangelnde Schnelligkeit. Wir können nicht wissen, wann ein Finalizer ausgeführt wird, da die Speicherbereinigung jederzeit erfolgen kann.

An sich ist dies kein Problem, da der Finalizer früher oder später noch ausgeführt wird. Die Systemressourcen sind jedoch nicht unbegrenzt. Daher gehen uns möglicherweise die Ressourcen aus, bevor eine Bereinigung durchgeführt wird, was zu einem Systemabsturz führen kann.

Finalizer wirken sich auch auf die Portabilität des Programms aus. Da der Garbage Collection-Algorithmus von der JVM-Implementierung abhängig ist, kann ein Programm auf einem System sehr gut ausgeführt werden, während es sich auf einem anderen System anders verhält.

Die Leistungskosten sind ein weiteres wichtiges Problem bei Finalisierern. Insbesondere muss JVM viel mehr Operationen ausführen, wenn Objekte erstellt und zerstört werden, die einen nicht leeren Finalizer enthalten .

Das letzte Problem, über das wir sprechen werden, ist das Fehlen einer Ausnahmebehandlung während der Finalisierung. Wenn ein Finalizer eine Ausnahme auslöst, wird der Finalisierungsprozess gestoppt und das Objekt ohne Benachrichtigung in einem beschädigten Zustand belassen.

3.2. Demonstration der Effekte von Finalisierern

Es ist Zeit, die Theorie beiseite zu legen und die Auswirkungen von Finalisierern in der Praxis zu sehen.

Definieren wir eine neue Klasse mit einem nicht leeren Finalizer:

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(""); } }

Beachten Sie die finalize () -Methode - sie druckt nur eine leere Zeichenfolge an die Konsole. Wenn diese Methode vollständig leer wäre, würde die JVM das Objekt so behandeln, als hätte es keinen Finalizer. Daher müssen wir finalize () eine Implementierung zur Verfügung stellen , die in diesem Fall fast nichts bewirkt.

Im Innern des Hauptverfahrens wird eine neue CrashedFinalizable Instanz wird in jeder Iteration der erstellt für Schleife. Diese Instanz ist keiner Variablen zugeordnet und daher für die Speicherbereinigung geeignet.

Fügen wir in der mit // anderem Code gekennzeichneten Zeile einige Anweisungen hinzu, um zu sehen, wie viele Objekte zur Laufzeit im Speicher vorhanden sind:

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

Die angegebenen Anweisungen greifen auf einige Felder in internen JVM-Klassen zu und drucken die Anzahl der Objektreferenzen nach jeweils einer Million Iterationen aus.

Beginnen wir das Programm, indem wir die Hauptmethode ausführen . Wir können erwarten, dass es auf unbestimmte Zeit läuft, aber das ist nicht der Fall. Nach einigen Minuten sollte das System mit einem ähnlichen Fehler abstürzen:

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

Es sieht so aus, als hätte der Garbage Collector seine Arbeit nicht gut gemacht - die Anzahl der Objekte stieg weiter an, bis das System abstürzte.

Wenn wir den Finalizer entfernen würden, wäre die Anzahl der Referenzen normalerweise 0 und das Programm würde für immer weiterlaufen.

3.3. Erläuterung

Um zu verstehen, warum der Garbage Collector Objekte nicht ordnungsgemäß verworfen hat, müssen wir uns ansehen, wie die JVM intern funktioniert.

Beim Erstellen eines Objekts, das auch als Referent bezeichnet wird und über einen Finalizer verfügt, erstellt die JVM ein zugehöriges Referenzobjekt vom Typ java.lang.ref.Finalizer . Nachdem der Referent für die Speicherbereinigung bereit ist, markiert die JVM das Referenzobjekt als verarbeitungsbereit und stellt es in eine Referenzwarteschlange.

Wir können auf diese Warteschlange über die statische Feldwarteschlange in der Klasse java.lang.ref.Finalizer zugreifen .

In der Zwischenzeit läuft ein spezieller Daemon-Thread namens Finalizer weiter und sucht nach Objekten in der Referenzwarteschlange. Wenn es eines findet, entfernt es das Referenzobjekt aus der Warteschlange und ruft den Finalizer für den Referenten auf.

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

In diesem Tutorial haben wir uns auf ein Kernkonzept in Java konzentriert - die Finalisierungsmethode . Dies sieht auf dem Papier nützlich aus, kann aber zur Laufzeit hässliche Nebenwirkungen haben. Und was noch wichtiger ist, es gibt immer eine alternative Lösung zur Verwendung eines Finalizers.

Ein kritischer Punkt ist, dass finalize ab Java 9 veraltet ist - und schließlich entfernt wird.

Wie immer finden Sie den Quellcode für dieses Tutorial auf GitHub.