Objektabschluss und Bereinigung

Vor drei Monaten begann ich eine Mini-Artikelserie über das Entwerfen von Objekten mit einer Diskussion über Gestaltungsprinzipien, die sich auf die richtige Initialisierung zu Beginn des Lebens eines Objekts konzentrierten. In diesem Design - Techniken Artikel werde ich auf den Design - Prinzipien konzentrieren , die helfen Ihnen die richtige Bereinigung am Ende eines Objekts Lebensdauer.

Warum aufräumen?

Jedes Objekt in einem Java-Programm verwendet endliche Rechenressourcen. Am offensichtlichsten ist, dass alle Objekte etwas Speicher verwenden, um ihre Bilder auf dem Heap zu speichern. (Dies gilt auch für Objekte, die keine Instanzvariablen deklarieren. Jedes Objektbild muss eine Art Zeiger auf Klassendaten enthalten und kann auch andere implementierungsabhängige Informationen enthalten.) Objekte können jedoch neben dem Speicher auch andere endliche Ressourcen verwenden. Beispielsweise können einige Objekte Ressourcen wie Dateihandles, Grafikkontexte, Sockets usw. verwenden. Wenn Sie ein Objekt entwerfen, müssen Sie sicherstellen, dass es schließlich alle von ihm verwendeten endlichen Ressourcen freigibt, damit dem System diese Ressourcen nicht ausgehen.

Da Java eine durch Müll gesammelte Sprache ist, ist es einfach, den mit einem Objekt verknüpften Speicher freizugeben. Sie müssen lediglich alle Verweise auf das Objekt loslassen. Da Sie sich nicht um die explizite Freigabe eines Objekts kümmern müssen, wie dies in Sprachen wie C oder C ++ der Fall sein muss, müssen Sie sich nicht um die Beschädigung des Speichers kümmern, indem Sie dasselbe Objekt versehentlich zweimal freigeben. Sie müssen jedoch sicherstellen, dass Sie tatsächlich alle Verweise auf das Objekt freigeben. Wenn Sie dies nicht tun, kann es zu einem Speicherverlust kommen, genau wie bei den Speicherverlusten in einem C ++ - Programm, wenn Sie vergessen, Objekte explizit freizugeben. Solange Sie jedoch alle Verweise auf ein Objekt freigeben, müssen Sie sich nicht darum kümmern, diesen Speicher explizit "freizugeben".

In ähnlicher Weise müssen Sie sich nicht darum kümmern, alle Bestandteile, auf die durch die Instanzvariablen eines nicht mehr benötigten Objekts verwiesen wird, explizit freizugeben. Durch das Freigeben aller Verweise auf das nicht benötigte Objekt werden alle in den Instanzvariablen dieses Objekts enthaltenen Objektverweise ungültig. Wenn die jetzt ungültig gemachten Verweise die einzigen verbleibenden Verweise auf diese konstituierenden Objekte waren, stehen die konstituierenden Objekte auch für die Speicherbereinigung zur Verfügung. Ein Kinderspiel, richtig?

Die Regeln der Speicherbereinigung

Obwohl die Speicherbereinigung die Speicherverwaltung in Java in der Tat viel einfacher macht als in C oder C ++, können Sie den Speicher beim Programmieren in Java nicht vollständig vergessen. Um zu wissen, wann Sie möglicherweise über die Speicherverwaltung in Java nachdenken müssen, müssen Sie ein wenig darüber wissen, wie die Speicherbereinigung in den Java-Spezifikationen behandelt wird.

Müllabfuhr ist nicht vorgeschrieben

Das Erste, was Sie wissen müssen, ist, dass Sie unabhängig davon, wie sorgfältig Sie die Java Virtual Machine-Spezifikation (JVM-Spezifikation) durchsuchen, keinen Befehlssatz finden können. Jede JVM muss über einen Garbage Collector verfügen. Die Java Virtual Machine-Spezifikation gibt VM-Designern viel Spielraum bei der Entscheidung, wie ihre Implementierungen den Speicher verwalten, einschließlich der Entscheidung, ob die Garbage Collection überhaupt verwendet werden soll oder nicht. Daher ist es möglich, dass einige JVMs (z. B. eine Bare-Bones-Smartcard-JVM) erfordern, dass in jeder Sitzung ausgeführte Programme in den verfügbaren Speicher "passen".

Natürlich kann Ihnen auch auf einem virtuellen Speichersystem immer der Speicher ausgehen. Die JVM-Spezifikation gibt nicht an, wie viel Speicher einer JVM zur Verfügung steht. Darin heißt es nur , dass , wenn eine JVM nicht genügend Speicherplatz aus, es soll eine werfen OutOfMemoryError.

Um Java-Anwendungen die beste Chance für die Ausführung zu bieten, ohne dass der Arbeitsspeicher knapp wird, verwenden die meisten JVMs einen Garbage Collector. Der Garbage Collector stellt den Speicher wieder her, der von nicht referenzierten Objekten auf dem Heap belegt wird, sodass der Speicher wieder von neuen Objekten verwendet werden kann, und zerlegt den Heap normalerweise während der Ausführung des Programms.

Der Garbage Collection-Algorithmus ist nicht definiert

Ein weiterer Befehl, den Sie in der JVM-Spezifikation nicht finden, ist Alle JVMs, die die Garbage Collection verwenden, müssen den XXX-Algorithmus verwenden. Die Designer jeder JVM können entscheiden, wie die Speicherbereinigung in ihren Implementierungen funktioniert. Der Garbage Collection-Algorithmus ist ein Bereich, in dem JVM-Anbieter bestrebt sein können, ihre Implementierung besser als die der Konkurrenz zu gestalten. Dies ist für Sie als Java-Programmierer aus folgendem Grund von Bedeutung:

Da Sie im Allgemeinen nicht wissen, wie die Garbage Collection in einer JVM ausgeführt wird, wissen Sie nicht, wann ein bestimmtes Objekt Garbage Collection wird.

Na und? du könntest fragen. Der Grund, warum es Sie interessieren könnte, wenn ein Objekt durch Müll gesammelt wird, hat mit Finalisierern zu tun. (Ein Finalizer ist definiert als eine reguläre Java-Instanzmethode mit dem Namen finalize()void, die void zurückgibt und keine Argumente akzeptiert.) Die Java-Spezifikationen versprechen Folgendes für Finalizer:

Bevor der von einem Objekt mit einem Finalizer belegte Speicher zurückgefordert wird, ruft der Garbage Collector den Finalizer des Objekts auf.

Da Sie nicht wissen, wann Objekte mit Müll gesammelt werden, aber Sie wissen, dass finalisierbare Objekte finalisiert werden, wenn sie mit Müll gesammelt werden, können Sie den folgenden großen Abzug vornehmen:

Sie wissen nicht, wann Objekte finalisiert werden.

Sie sollten diese wichtige Tatsache in Ihr Gehirn einprägen und es für immer zulassen, dass es Ihre Java-Objektdesigns informiert.

Finalizer zu vermeiden

Die zentrale Faustregel für Finalizer lautet:

Entwerfen Sie Ihre Java-Programme nicht so, dass die Richtigkeit von der "rechtzeitigen" Fertigstellung abhängt.

Mit anderen Worten, schreiben Sie keine Programme, die brechen, wenn bestimmte Objekte nicht durch bestimmte Punkte im Leben der Programmausführung finalisiert werden. Wenn Sie ein solches Programm schreiben, funktioniert es möglicherweise bei einigen Implementierungen der JVM, schlägt jedoch bei anderen fehl.

Verlassen Sie sich nicht auf Finalizer, um Nicht-Speicherressourcen freizugeben

Ein Beispiel für ein Objekt, das gegen diese Regel verstößt, ist ein Objekt, das eine Datei in seinem Konstruktor öffnet und die Datei in seiner finalize()Methode schließt . Obwohl dieses Design ordentlich, ordentlich und symmetrisch erscheint, erzeugt es möglicherweise einen heimtückischen Fehler. Ein Java-Programm verfügt im Allgemeinen nur über eine begrenzte Anzahl von Dateihandles. Wenn alle diese Handles verwendet werden, kann das Programm keine weiteren Dateien öffnen.

Ein Java-Programm, das ein solches Objekt verwendet (eines, das eine Datei in seinem Konstruktor öffnet und in seinem Finalizer schließt), funktioniert möglicherweise bei einigen JVM-Implementierungen einwandfrei. Bei solchen Implementierungen würde die Finalisierung häufig genug erfolgen, um jederzeit eine ausreichende Anzahl von Dateihandles verfügbar zu halten. Das gleiche Programm kann jedoch auf einer anderen JVM fehlschlagen, deren Garbage Collector nicht oft genug finalisiert wird, um zu verhindern, dass dem Programm die Dateihandles ausgehen. Oder, was noch heimtückischer ist, das Programm funktioniert jetzt möglicherweise auf allen JVM-Implementierungen, schlägt jedoch in einer geschäftskritischen Situation einige Jahre (und Release-Zyklen) später fehl.

Andere Faustregeln für den Finalizer

Zwei weitere Entscheidungen, die JVM-Designern überlassen bleiben, sind die Auswahl des Threads (oder der Threads), in dem die Finalizer ausgeführt werden, und die Reihenfolge, in der die Finalizer ausgeführt werden. Finalizer können in beliebiger Reihenfolge ausgeführt werden - nacheinander von einem einzelnen Thread oder gleichzeitig von mehreren Threads. Wenn Ihr Programm in Bezug auf die Richtigkeit davon abhängt, ob Finalizer in einer bestimmten Reihenfolge oder von einem bestimmten Thread ausgeführt werden, funktioniert es möglicherweise bei einigen JVM-Implementierungen, schlägt jedoch bei anderen fehl.

Sie sollten auch berücksichtigen, dass Java ein Objekt als finalisiert betrachtet, unabhängig davon, ob die finalize()Methode normal zurückgegeben wird oder abrupt durch Auslösen einer Ausnahme abgeschlossen wird. Garbage Collectors ignorieren alle von Finalisierern ausgelösten Ausnahmen und benachrichtigen den Rest der Anwendung in keiner Weise darüber, dass eine Ausnahme ausgelöst wurde. Wenn Sie sicherstellen möchten, dass ein bestimmter Finalizer eine bestimmte Mission vollständig erfüllt, müssen Sie diesen Finalizer so schreiben, dass er alle Ausnahmen behandelt, die auftreten können, bevor der Finalizer seine Mission abschließt.

Eine weitere Faustregel für Finalizer betrifft Objekte, die am Ende der Lebensdauer der Anwendung auf dem Heap verbleiben. Standardmäßig führt der Garbage Collector die Finalizer von Objekten, die beim Beenden der Anwendung auf dem Heap verbleiben, nicht aus. Um diese Standardeinstellung zu ändern, müssen Sie die runFinalizersOnExit()Methode class Runtimeoder aufrufen System, die trueals einzelner Parameter übergeben wird. Wenn Ihr Programm Objekte enthält, deren Finalizer vor dem Beenden des Programms unbedingt aufgerufen werden müssen, müssen Sie runFinalizersOnExit()irgendwo in Ihrem Programm aufrufen .

Wofür sind Finalizer gut?

Inzwischen haben Sie möglicherweise das Gefühl, dass Sie für Finalizer nicht viel verwenden können. Während es wahrscheinlich ist, dass die meisten von Ihnen entworfenen Klassen keinen Finalizer enthalten, gibt es einige Gründe, Finalizer zu verwenden.

Eine vernünftige, wenn auch seltene Anwendung für einen Finalizer besteht darin, Speicher freizugeben, der durch native Methoden zugewiesen wird. Wenn ein Objekt eine native Methode aufruft, die Speicher zuweist (möglicherweise eine C-Funktion, die aufruft malloc()), kann der Finalizer dieses Objekts eine native Methode aufrufen, die diesen Speicher freigibt (Aufrufe free()). In dieser Situation würden Sie den Finalizer verwenden, um Speicher freizugeben, der für ein Objekt zugewiesen wurde - Speicher, der vom Garbage Collector nicht automatisch zurückgefordert wird.

Eine andere, häufigere Verwendung von Finalisierern besteht darin, einen Fallback-Mechanismus zum Freigeben von nicht speicherinternen endlichen Ressourcen wie Dateihandles oder Sockets bereitzustellen. Wie bereits erwähnt, sollten Sie sich nicht auf Finalizer verlassen, um endliche Nicht-Speicherressourcen freizugeben. Stattdessen sollten Sie eine Methode bereitstellen, mit der die Ressource freigegeben wird. Möglicherweise möchten Sie aber auch einen Finalizer hinzufügen, der überprüft, ob die Ressource bereits freigegeben wurde. Wenn dies nicht der Fall ist, wird sie freigegeben. Ein solcher Finalizer schützt vor dem schlampigen Gebrauch Ihrer Klasse (und wird ihn hoffentlich nicht fördern). Wenn ein Client-Programmierer vergisst, die von Ihnen bereitgestellte Methode zum Freigeben der Ressource aufzurufen, gibt der Finalizer die Ressource frei, falls das Objekt jemals durch Müll gesammelt wird. Die finalize()Methode derLogFileManager Die Klasse, die später in diesem Artikel gezeigt wird, ist ein Beispiel für diese Art von Finalizer.

Vermeiden Sie Finalizer-Missbrauch

Das Vorhandensein der Finalisierung führt zu einigen interessanten Komplikationen für JVMs und einigen interessanten Möglichkeiten für Java-Programmierer. Die Finalisierung gewährt Programmierern die Macht über Leben und Tod von Objekten. Kurz gesagt, es ist in Java möglich und völlig legal, Objekte in Finalisatoren wiederzubeleben - um sie wieder zum Leben zu erwecken, indem sie erneut referenziert werden. (Eine Möglichkeit, wie ein Finalizer dies erreichen kann, besteht darin, einen Verweis auf das zu finalisierende Objekt zu einer statischen verknüpften Liste hinzuzufügen, die noch "live" ist.) Obwohl eine solche Leistung möglicherweise verlockend ist, weil Sie sich dadurch wichtig fühlen, gilt die Faustregel ist der Versuchung zu widerstehen, diese Kraft zu nutzen. Im Allgemeinen stellt die Wiederbelebung von Objekten in Finalisierern einen Missbrauch des Finalisierers dar.

Die Hauptbegründung für diese Regel ist, dass jedes Programm, das die Auferstehung verwendet, in ein leichter verständliches Programm umgestaltet werden kann, das keine Auferstehung verwendet. Ein formaler Beweis dieses Theorems wird dem Leser als Übung überlassen (das wollte ich schon immer sagen), aber in einem informellen Geist sollten Sie bedenken, dass die Objektauferstehung so zufällig und unvorhersehbar sein wird wie die Objektfinalisierung. Daher wird ein Entwurf, der die Auferstehung verwendet, für den nächsten Wartungsprogrammierer, der mitmacht, schwer herauszufinden sein - der möglicherweise die Besonderheiten der Speicherbereinigung in Java nicht vollständig versteht.

Wenn Sie der Meinung sind, dass Sie ein Objekt einfach wieder zum Leben erwecken müssen, sollten Sie eine neue Kopie des Objekts klonen, anstatt dasselbe alte Objekt wiederzubeleben. Der Grund für diesen Ratschlag ist, dass Garbage Collectors in der JVM die finalize()Methode eines Objekts nur einmal aufrufen . Wenn dieses Objekt wiederbelebt wird und ein zweites Mal für die Speicherbereinigung verfügbar ist, wird die finalize()Methode des Objekts nicht erneut aufgerufen.