Eine Anleitung zur Finalisierungsmethode in Java

Eine Anleitung zur Finalize-Methode in Java

1. Überblick

In diesem Tutorial konzentrieren wir uns auf einen Kernaspekt der Java-Sprache - diefinalize-Methode, die von der Root-KlasseObjectbereitgestellt wird.

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

2. Finalizer verwenden

Diefinalize()-Methode wird als Finalizer bezeichnet.

Finalizer werden aufgerufen, wenn JVM feststellt, dass für diese bestimmte Instanz Speicherbereinigungen erforderlich sind. Ein solcher Finalizer kann alle Vorgänge 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 Hauptmechanismus für Aufräumarbeiten oder als Sicherheitsnetz dienen, 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 KlasseFinalizable hat ein Feldreader, das auf eine verschließbare Ressource verweist. Wenn ein Objekt aus dieser Klasse erstellt wird, erstellt es eine neueBufferedReader-Instanz, die aus einer Datei im Klassenpfad liest.

Eine solche Instanz wird in der MethodereadFirstLine verwendet, um die erste Zeile in der angegebenen Datei zu extrahieren. Notice that the reader isn’t closed in the given code.

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 Wirklichkeitthe time at which the garbage collector calls finalizers is dependent on the JVM’s implementation and the system’s conditions, which are out of our control.

Um die Speicherbereinigung vor Ort zu ermöglichen, nutzen wir dieSystem.gc-Methode. In realen Systemen sollten wir das aus mehreren Gründen niemals explizit aufrufen:

  1. Es ist teuer

  2. Die Garbage Collection wird nicht sofort ausgelöst, sondern 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ürjconsole 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("example.com", firstLine);
    System.gc();
}

In der ersten Anweisung wird einFinalizable-Objekt erstellt und anschließend diereadFirstLine-Methode aufgerufen. Dieses Objekt ist keiner Variablen zugeordnet und kann daher beim Aufrufen der MethodeSystem.gcfür die Speicherbereinigung verwendet werden.

Die Behauptung 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 darüber gedruckt, dass der gepufferte Reader im Finalizer geschlossen wird. Dies impliziert, dass die Methodefinalizeaufgerufen wurde und die Ressource bereinigt wurde.

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

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

3. Finalizer vermeiden

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

Das erste erkennbare Problem im Zusammenhang mit Finalisierern ist die mangelnde Schnelligkeit. Wir können nicht wissen, wann ein Finalizer ausgeführt wird, da die Garbage Collection jederzeit stattfinden kann.

An sich ist dies kein Problem, da das Wichtigste ist, dass der Finalizer früher oder später immer noch aufgerufen wird. Die Systemressourcen sind jedoch begrenzt. Thus, we may run out of those resources before they get a chance to be cleaned up, potentially resulting in system crashes.

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 zur Laufzeit auf einem anderen System anders verhält.

Ein weiteres wichtiges Problem bei Finalisierern sind die Leistungskosten. InsbesondereJVM must perform much more operations when constructing and destroying objects containing a non-empty finalizer.

Die Details sind implementierungsspezifisch, aber die allgemeinen Ideen sind in allen JVMs gleich: Es müssen zusätzliche Schritte unternommen werden, um sicherzustellen, dass Finalizer ausgeführt werden, bevor die Objekte verworfen werden. Diese Schritte können dazu führen, dass sich die Dauer der Objekterstellung und -zerstörung um das Hundert- oder sogar Tausendfache erhöht.

Das letzte Problem, über das wir sprechen werden, ist das Fehlen einer Ausnahmebehandlung während der Finalisierung. If a finalizer throws an exception, the finalization process is canceled, and the exception is ignored, leaving the object in a corrupted state ohne Benachrichtigung.

4. No-Finalizer-Beispiel

Lassen Sie uns eine Lösung erforschen, die die gleiche Funktionalität bereitstellt, aber ohne die Methodefinalize(). Beachten Sie, dass das folgende Beispiel nicht die einzige Möglichkeit ist, Finalizer zu ersetzen.

Stattdessen wird ein wichtiger Punkt aufgezeigt: Es gibt immer Optionen, mit denen wir Finalisierer vermeiden können.

Hier ist die Erklärung unserer neuen Klasse:

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

Es ist nicht schwer zu erkennen, dass der einzige Unterschied zwischen der neuenCloseableResource-Klasse und unserer vorherigenFinalizable-Klasse in der Implementierung derAutoCloseable-Schnittstelle anstelle einer Finalizer-Definition besteht.

Beachten Sie, dass der Körper derclose-Methode vonCloseableResource fast der gleiche ist wie der Körper des Finalisierers in der KlasseFinalizable.

Die folgende Testmethode liest eine Eingabedatei und gibt die Ressource nach Abschluss des Jobs frei:

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

Im obigen Test wird eineCloseableResource-Instanz imtry-Block einer Try-with-Resources-Anweisung erstellt. Daher wird diese Ressource automatisch geschlossen, wenn der Try-with-Resources-Block die Ausführung abgeschlossen hat.

Wenn Sie die angegebene Testmethode ausführen, wird eine Meldung angezeigt, die aus der Methodecloseder KlasseCloseableResourceausgedruckt wurde.

5. Fazit

In diesem Tutorial haben wir uns auf ein Kernkonzept in Java konzentriert - diefinalize-Methode. Dies sieht auf dem Papier nützlich aus, kann jedoch 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, dassfinalize ab Java 9 veraltet ist - und schließlich entfernt wird.

Der Quellcode für dieses Tutorial befindet sich wie immer inover on GitHub.