Java Blog

Insel-Update: Aufräumen mit finalize()

Wenn die automatische Speicherbereinigung feststellt, dass es keine Referenz mehr auf ein bestimmtes Objekt gibt, so ruft sie automatisch die besondere Methode finalize() auf diesem Objekt auf. Danach kann die automatische Speicherbereinigung das Objekt entfernen. Wir können diese Methode für eigene Aufräumarbeiten überschreiben, die Finalizer genannt wird (ein Finalizer hat nichts mit dem finally-Block einer Exception-Behandlung zu tun).

Seit Java 9 ist die Methode deprecated[1], um Entwickler zu ermutigen, auf diese Methode zu verzichten, obwohl sie natürlich weiterhin von der JVM aufgerufen wird. Es gibt mehrere Probleme. Einige Entwickler verstehen die Arbeitsweise der Methode nicht richtig und denken, sie wird immer aufgerufen. Doch hat die virtuelle Maschine Fantastillionen Megabyte an Speicher zur Verfügung und wird dann beendet, gibt sie den Heap-Speicher als Ganzes dem Betriebssystem zurück. In so einem Fall gibt es keinen Grund für eine automatische Speicherbereinigung als Grabträger und Folglich kein Aufruf von finalize(). Und wann genau der Garbage-Collector in Aktion tritt, ist auch nicht vorhersehbar, sodass im Gegensatz zu C++ in Java keine Aussage über den Zeitpunkt möglich ist, zu dem das Laufzeitsystem finalize() aufruft – alles ist vollständig nicht-deterministisch und von der Implementierung der automatischen Speicherbereinigung abhängig. Üblicherweise werden Objekte mit finalize() von einem Extra-Garbage-Collector behandelt, und der arbeitet langsamer als der normale GC, was somit ein Nachteil ist.

Sprachenvergleich

Einen Destruktor, der wie in C++ am Ende eines Gültigkeitsbereichs einer Variablen aufgerufen wird, gibt es in Java nicht.

 

class java.lang.Object

Einmal Finalizer, vielleicht mehrmals die automatische Speicherbereinigung

Objekte von Klassen, die eine finalize()-Methode besitzen, kann Oracles JVM nicht so schnell erzeugen und entfernen wie Klassen ohne finalize(). Das liegt auch daran, dass die automatische Speicherbereinigung vielleicht mehrmals laufen muss, um das Objekt zu löschen. Es gilt zwar, dass der Garbage-Collector aus dem Grund finalize() aufruft, weil das Objekt nicht mehr benötigt wird, es kann aber sein, dass die finalize()-Methode die this-Referenz nach außen gibt, sodass das Objekt wegen einer bestehenden Referenz nicht gelöscht werden kann und so zurück von den Toten geholt wird. Das Objekt wird zwar irgendwann entfernt, aber der Finalizer läuft nur einmal und nicht immer pro GC-Versuch.[2]

Löst eine Anweisung in finalize() eine Ausnahme aus, so wird diese ignoriert. Das bedeutet aber, dass die Finalisierung des Objekts stehen bleibt. Die automatische Speicherbereinigung beeinflusst das in ihrer Arbeit aber nicht.

super.finalize()

Überschreiben wir in einer Unterklasse finalize(), dann müssen wir auch gewährleisten, dass die Methode finalize() der Oberklasse aufgerufen wird. So besitzt zum Beispiel die Klasse Font ein finalize(), das durch eine eigene Implementierung nicht verschwinden darf. Wir müssen daher in unserer Implementierung super.finalize() aufrufen (es wäre gut, wenn der Compiler das wie beim Konstruktoraufruf immer automatisch machen würde). Leere finalize()-Methoden ergeben im Allgemeinen keinen Sinn, es sei denn, das finalize() der Oberklasse soll explizit übergangen werden:

@Override protected void finalize() throws Throwable {
try {
// …
}
finally {
super.finalize();
}
}

Der Block vom finally wird immer ausgeführt, auch wenn es im oberen Teil eine Ausnahme gab.

Die Methode von Hand aufzurufen, ist ebenfalls keine gute Idee, denn das kann zu Problemen führen, wenn der GC-Thread die Methode auch gerade aufruft. Um das Aufrufen von außen einzuschränken, sollte die Sichtbarkeit von protected bleiben und nicht erhöht werden.

Hinweis

Da beim Programmende vielleicht nicht alle finalize()-Methoden abgearbeitet wurden, haben die Entwickler schon früh einen Methodenaufruf System.runFinalizersOnExit(true); vorgesehen. Mittlerweile ist die Methode veraltet und sollte auf keinen Fall mehr aufgerufen werden. Die API-Dokumentation erklärt:

„It may result in finalizers being called on live objects while other threads are concurrently manipulating those objects, resulting in erratic behavior or deadlock.“

Dazu auch Joshua Bloch, Autor des ausgezeichneten Buchs „Effective Java Programming Language Guide“:

„Never call System.runFinalizersOnExit or Runtime.runFinalizersOnExit for any reason: they are among the most dangerous methods in the Java libraries.“

Gültige Alternativen

Gedacht war die überschriebene Methode finalize() um wichtige Ressourcen zur Not freizugeben, etwa File-Handles via close() oder Grafikkontexte des Betriebssystems, wenn der Entwickler das vergessen hat. Alle diese Freigaben müssten eigentlich vom Entwickler angestoßen werden, und finalize() ist nur ein Helfer, der rettend eingreifen kann. Doch da die automatische Speicherbereinigung finalize() nur dann aufruft, wenn sie tote Objekte freigeben möchte, dürfen wir uns nicht auf die Ausführung verlassen. Gehen zum Beispiel die File-Handles aus, wird der Garbage-Collector nicht aktiv; es erfolgen keine finalize()-Aufrufe, und nicht mehr erreichbare, aber noch nicht weggeräumte Objekte belegen weiter die knappen File-Handles. Es muss also ein Mechanismus her, der korrekt ist und und immer funktioniert. Hier gibt es zwei Ansätze:

  1. try-mit-Ressourcen ist ein eleganter Weg, damit auf Ressourcen die close()-Methode aufgerufen wird. Nachteil ist, dass die genutzte Ressource das selbst nicht weiß, ob der Nutzer sie mit close()
  2. Die unter Java 9 eingeführte Klasse lang.ref.Cleaner hilft beim Aufräumen, dazu später mehr in diesem Kapitel.

 

[1]      https://bugs.openjdk.java.net/browse/JDK-8165641

[2] Einige Hintergründe erfährt der Leser unter http://www.iecc.com/gclist/GC-lang.html#Finalization.