Hallo,
@ISliceUrPanties hat diese beiden Videos als Hilfestellung gefunden, wenn es um Verständnis bezüglich GarbageCollection und Dispose-Pattern geht. GarbageCollection, Dispose-Pattern
Diese sind englisch und c#. Deswegen hier eine Zusammenfassung in deutsch und vb.net. Ich bin da auch kein Fachmann, daher kann ich nur darbringen, was die Videos vermitteln, nicht mehr.
GarbageCollection:
Es wird nicht darauf eingegangen, was der Stack oder was der Heap sind. Sie werden zur Veranschaulichung benutzt wann Daten vorhanden sind und wann nicht.
Benutzter Code:
Eine
Außerdem überschreibt die Person, ihre
Programm
In
Am Ende der
Das Objekt "Fred" verweist über seine
Wird die Methode verlassen, verschwinden
Bei der GarbageCollection geht der GC durch alle lokalen Variablen und shared Variablen im Stack und markiert als erreichbar
- die Daten, auf welche diese Variablen verweisen,
- und die Daten, auf welche die markierten Daten verweisen.
In unserer ersten GarbageCollection befindet sich nur
Entsprechend erhalten wir an dieser Stelle den Output "Collecting Fred."
Verlassen wir nun die Methode
Was in unserem Beispielcode nur die Ausgabe "Collecting Bamm-Bamm.", "Collecting Pebbles.", "Collecting Wilma" bedeutet. Hier sieht man auch, dass erst der Inhalt von "Wilma" vernichtet wird und dann "Wilma" selbst.
---------------------------------------------------------------------------------------------------
Effizienz der GarbageCollection:
Das echte System bedient sich eines Prinzips, das die Effizienz verbessert.
Eigentlich gibt es 4 Heaps. Generation 0-, 1- und 2- und den Large-Object-Heap. Der Large-Object-Heap beinhaltet Objekte > 85KB
Der GC läuft wie gesagt automatisch und schaut dabei unterschiedlich häufig in die unterschiedlichen Heaps.
Am häufigsten wird der Generation 0-Heap überprüft. Alle Objekte die erreichbar sind, werden in den Generation 1-Heap verfrachtet. Anschließend wird der Generation 0-Heap gar nicht gelöscht, sondern der Heap-Zeiger wird wieder auf den Anfang gesetzt, was neue Daten alles überschreiben lässt, was da ist oder auch nicht da ist.
Nichtmehr ganz so häufig wird der Generation 1-Heap überprüft, denn da sind ja nur Objekte drin, die schon mindestens einmal gezeigt haben, dass sie eine gewisse Dauer überstehen. Hier bleibt der Ablauf gleich.
Der Generation 2-Heap wird nur selten geprüft. Der Ablauf entspricht hier wie weiter oben geschildert; d.h. hier müssen die Objekte wirklich gelöscht werden, was daher etwas länger dauert. Der Large-Object-Heap verhält sich wie der Generation 2-Heap, basierend auf der Annahme "große Objekte sind langlebige Objekte". Deswegen kommt großen Objekten gegebenenfalls eine besondere Bedeutung zu, wenn sie mal nicht besonders langlebig sein sollten, denn sie werden trotzdem nur selten vom GC angeguckt.
Sowohl das Nicht-Löschen, als auch die statistisch plausible Verteilung der Überprüfungsfrequenz auf die verschiedenen Heaps sorgen für eine gute Effizienz.
---------------------------------------------------------------------------------------------------
Disposen:
Wir haben gesehen, dass der GC Objekte Finalized. Normalerweise wird ein
Am meisten hat er Auswirkungen dann, wenn man das Disposen vergisst. Denn Finalizen ist an einigen Stellen nicht effizient, weil es nicht in unserer Hand liegt, denn der GC soll unabhängig vom Programmierer arbeiten.
Also in erster Linie bedeutet die Implementation von
Mit
Benutzter Code:
Fall1
Ein
Befindet sich der
Wir müssen also den Zeitpunkt der Zerstörung einer
Unser Programm erzeugt nur eine Instanz unserer Klasse und "benutzt" sie.
Außerdem forcieren wir erneut eine
werden (Den ruft der GC auf). Er ist auskommentiert, da er an dieser Stelle nicht zur Implementation von
Der Using-Block um
Lassen wir den Using-Block weg und kommentieren den
Also es ändert sich nur wer aufräumt. Das sieht man auch, wenn man durch den Code stepped: "Disposing" erscheint am Ende des Using-Blocks, "Finalizing" erscheint beim
Also Using wieder rein. Und siehe da: jetzt passiert natürlich beides, "Disposing" und "Finalizing" erscheinen. Das will man nicht, aus Effizienz-Gründen.
Daher wird in die
Warum ist der Finalizer ineffizient? Wir haben in der Betrachtung der Heaps gesehen, das setzen des Heap-Pointer an den Anfang des Heaps erspart enorm viel Arbeit, aber so ein Objekt wie StreamWriter will gelöscht werden. Denn wir können ja nicht warten bis der von neuem Speicherbedarf überschrieben wird. Finalizer stoppen also das generelle Prinzip der schnellen Heap-Bereinigung. Sie reihen sich in eine Finalizing-Warteschlange auch dann, wenn man den
Ein kleines Spielchen noch:
Es gilt in Klassen, deren Bestandteile alle verwaltet sind, braucht man keinen
Das bringt uns zu Klassen, die nicht-verwaltete Bestandteile enthalten.
---------------------------------------------------------------------------------------------------
Dispose-Pattern:
Wenn wir nicht-verwaltete Ressourcen nutzen, müssen wir sie aufräumen, denn das tun diese genau nicht mehr selbst und dafür bieten sich entsprechende Methodenaufrufe im Finalizer an, denn das ist die Methode die der GC aufruft.
Dadurch kann das System (wenn nötig) automatisch aufräumen. Wir wollen aber auch weiterhin selbst aufräumen.
Aber verwaltete Ressourcen haben wir höchstwahrscheinlich auch noch, dann geht das schon nicht mehr, weil wir die ja nicht im Finalizer bereinigen können, da dies wie bei (*) gesehen zur Exception führt. Also muss das außerhalb passieren.
Unsere Dispose-Methode muss also:
- verwaltete und nicht verwaltete Ressourcen manuell bereinigbar machen
- nicht verwaltete Ressourcen automatisch bereinigbar machen
- verwaltete Ressourcen nicht versuchen in der automatischen Bereinigung zu bereinigen.
Fall2
Wenn wir
- verwaltete und nicht verwaltete Ressourcen manuell bereinigbar gemacht (Sowohl der Code für
Wir überschreiben den Finalizer, dadurch bekommt der GC Zugang zu Methoden, die er sonst nicht hat; also werden die
- nicht verwalteten Ressourcen automatisch bereinigbar gemacht.
Da wir einen Finalizer nun verwenden müssen, im Gegensatz zu Fall 1, wollen wir im echten
Der Finalizer ruft die
- es wird nicht versucht verwaltete Ressourcen in der automatischen Bereinigung zu bereinigen
Die Bereinigung von Excel hat spezielle Methoden (
Die Abfrage von
So das ist was ich nun endlich mitnehmen konnte
--------------------------------------------------------------------------------------
Folgende Fehler möchte ich noch anmerken im Beispiel-Code (Ich möchte sie nicht im Code einfach ersetzen, da im Video derselbe "falsche" Code verwendet wird):
Fall2 benötigt in Zeile 36-39 am Ende noch
Es gibt Fälle in denen
Danke @ErfinderDesRades
Viele Grüße
@ISliceUrPanties hat diese beiden Videos als Hilfestellung gefunden, wenn es um Verständnis bezüglich GarbageCollection und Dispose-Pattern geht. GarbageCollection, Dispose-Pattern
Diese sind englisch und c#. Deswegen hier eine Zusammenfassung in deutsch und vb.net. Ich bin da auch kein Fachmann, daher kann ich nur darbringen, was die Videos vermitteln, nicht mehr.
GarbageCollection:
Es wird nicht darauf eingegangen, was der Stack oder was der Heap sind. Sie werden zur Veranschaulichung benutzt wann Daten vorhanden sind und wann nicht.
Benutzter Code:
Eine
Person
hat einen Namen und beispielsweise zwei Kinder, (sinnvollererweise eine List
an Kindern, aber hier gehts nicht um den Sinn der Klasse)Außerdem überschreibt die Person, ihre
Finalize
-Methode, welche jedes Object
besitzt. Die Überschreibung ist hier ebenfalls keine sinnvolle Überschreibung, sie soll explizit nur den Aufruf der Methode sichtbar machen.VB.NET-Quellcode
- Class Programm
- Private Shared Sub ErzeugeLeben(elternteil As Person)
- Dim fred As Person = New Person With {
- .Name = "Fred",
- .KindEins = New Person With {.Name = "Bamm-Bamm"}
- }
- parent.KindZwei = fred.KindEins
- End Sub
- Private Shared Sub Run()
- Dim wilma As Person = New Person With {
- .Name = "Wilma",
- .KindEins = New Person With {.Name = "Pebbles"}
- }
- ErzeugeLeben(wilma)
- Console.Writeline("Leaving 'ErzeugeLeben'...")
- GC.Collect()
- GC.WaitForPendingFinalizers()
- End Sub
- Private Shared Sub Main()
- Run()
- Console.Writeline(vbLf & "Leaving 'Run'...")
- GC.Collect()
- GC.WaitForPendingFinalizers()
- End Sub
- End Class
In
Run()
wird wilma
, mit bereits einem Kind erzeugt. ErzeugeLeben()
fügt wilma
ein weiteres Kind hinzu, dazu wird eine weitere Person benötigt, welche kurzfristig in dieser Methode erzeugt (fred
) wird. fred
s erstes Kind wird zu wilma
s zweitem Kind.GC.Collect
wird normalerweise nicht in angewandtem Code benötigt. Denn diese Methode wird vom System automatisch ausgelöst, wenn sie benötigt wird. Hier soll sie die Collection zu dem Zeitpunkt erzwingen wie er für uns nützlich ist, um zu sehen was passiert. Ein solcher Zeitpunkt ist nach Ende der Methode ErzeugeLeben
und ein weiterer nach der Methode Run
. GC.Collect
aktiviert automatisch die Finalizer
Methode von Objekten. Aber von welchen Objekten?Am Ende der
ErzeugeLeben
Methode befinden sich im Stack die Variablen fred
, parent
und wilma
, wobei fred
auf das Objekt "Fred" und parent
und wilma
auf das Objekt "Wilma" im Heap verweisen.Das Objekt "Fred" verweist über seine
KindEins
-Property außerdem auf das Objekt "Bamm-Bamm" und Objekt "Wilma" verweist in gleicher Manier auf die Objekte "Pebbles" und "Bamm-Bamm" im Heap.Wird die Methode verlassen, verschwinden
fred
und parent
aus dem Stack.Bei der GarbageCollection geht der GC durch alle lokalen Variablen und shared Variablen im Stack und markiert als erreichbar
- die Daten, auf welche diese Variablen verweisen,
- und die Daten, auf welche die markierten Daten verweisen.
In unserer ersten GarbageCollection befindet sich nur
wilma
im Stack, markiert wird daher "Wilma" und daher "Pebbles" und "Bamm-Bamm". Anschließend werden die unmarkierten Daten entfernt, das heißt deren Finalizer
wird aufgerufenEntsprechend erhalten wir an dieser Stelle den Output "Collecting Fred."
Verlassen wir nun die Methode
Run
verschwindet auch wilma
aus dem Stack. Wir stoßen erneut ein Collection an und nun bekommen die Objekte "Wilma", "Pebbles" und "Bamm-Bamm" keine Markierung mehr, sie werden entfernt.Was in unserem Beispielcode nur die Ausgabe "Collecting Bamm-Bamm.", "Collecting Pebbles.", "Collecting Wilma" bedeutet. Hier sieht man auch, dass erst der Inhalt von "Wilma" vernichtet wird und dann "Wilma" selbst.
---------------------------------------------------------------------------------------------------
Effizienz der GarbageCollection:
Das echte System bedient sich eines Prinzips, das die Effizienz verbessert.
Eigentlich gibt es 4 Heaps. Generation 0-, 1- und 2- und den Large-Object-Heap. Der Large-Object-Heap beinhaltet Objekte > 85KB
Der GC läuft wie gesagt automatisch und schaut dabei unterschiedlich häufig in die unterschiedlichen Heaps.
Am häufigsten wird der Generation 0-Heap überprüft. Alle Objekte die erreichbar sind, werden in den Generation 1-Heap verfrachtet. Anschließend wird der Generation 0-Heap gar nicht gelöscht, sondern der Heap-Zeiger wird wieder auf den Anfang gesetzt, was neue Daten alles überschreiben lässt, was da ist oder auch nicht da ist.
Nichtmehr ganz so häufig wird der Generation 1-Heap überprüft, denn da sind ja nur Objekte drin, die schon mindestens einmal gezeigt haben, dass sie eine gewisse Dauer überstehen. Hier bleibt der Ablauf gleich.
Der Generation 2-Heap wird nur selten geprüft. Der Ablauf entspricht hier wie weiter oben geschildert; d.h. hier müssen die Objekte wirklich gelöscht werden, was daher etwas länger dauert. Der Large-Object-Heap verhält sich wie der Generation 2-Heap, basierend auf der Annahme "große Objekte sind langlebige Objekte". Deswegen kommt großen Objekten gegebenenfalls eine besondere Bedeutung zu, wenn sie mal nicht besonders langlebig sein sollten, denn sie werden trotzdem nur selten vom GC angeguckt.
Sowohl das Nicht-Löschen, als auch die statistisch plausible Verteilung der Überprüfungsfrequenz auf die verschiedenen Heaps sorgen für eine gute Effizienz.
---------------------------------------------------------------------------------------------------
Disposen:
Wir haben gesehen, dass der GC Objekte Finalized. Normalerweise wird ein
Finalizer
nur selten angefasst. Am meisten hat er Auswirkungen dann, wenn man das Disposen vergisst. Denn Finalizen ist an einigen Stellen nicht effizient, weil es nicht in unserer Hand liegt, denn der GC soll unabhängig vom Programmierer arbeiten.
Also in erster Linie bedeutet die Implementation von
Dispose
nur einen Effizienzgewinn. (Überwiegend was die Memory betrifft und Blockaden bestimmter Zugriffe, denn wie wir nun wissen, wann der Finalizer
kommt, bestimmen normalerweise nicht wir)Mit
Dispose
bestimmen wir selbst.Benutzter Code:
VB.NET-Quellcode
- Imports System.IO
- Class Programm
- Private Shared Sub Run()
- Using kvk = New KomplettVerwalteteKlasse()
- kvk.StartWriting()
- End Using
- End Sub
- Private Shared Sub Main()
- Run()
- GC.Collect()
- GC.WaitPendingFinalizers()
- End Sub
- End Class
- Public Class KomplettVerwalteteKlasse
- Implements IDisposable
- Private _writer As StreamWriter
- Public Sub StartWriting()
- _writer = New StreamWriter("Ausgabe.txt")
- End Sub
- Public Sub Dispose() Implements IDisposable.Dispose
- Console.Writeline("Disposing")
- _writer?.Dispose()
- End Sub
- ' Protected Overrides Sub Finalize()
- ' Console.WriteLine("Finalizing")
- ' End Sub
- End Class
Ein
StreamWriter
öffnet eine Datei und blockiert damit den Zugriff. Um den Zugriff wiederzuerlangen, muss der StreamWriter
zerstört werden. Dazu implementiert diese Klasse bereits die IDisposable
Schnittstelle und ermöglicht durch Aufruf der Dispose
-Methode diesen Zeitpunkt zu bestimmen.Befindet sich der
StreamWriter
in einer weiteren Klasse, hier im Beispiel die KomplettVerwalteteKlasse
, dann haben wir gesehen, hat diese Klasse einen Verweis auf den StreamWriter
und ist somit für seine Lebensspanne verantwortlich.Wir müssen also den Zeitpunkt der Zerstörung einer
KomplettVerwalteteKlasse
-Instanz bestimmen können, damit wir die Zerstörung des StreamWriter
bestimmen können. Also implementieren wir hier nun die IDisposable-Schnittstelle, denn die macht ja genau das. Dabei reicht es vollkommen aus das Dispose
des StreamWriters
zu kaskadieren. Eine Ausgabe in die Konsole hilft uns hier wieder den Vorgang zu betrachten.Unser Programm erzeugt nur eine Instanz unserer Klasse und "benutzt" sie.
Außerdem forcieren wir erneut eine
GarbageCollection
, damit wir wieder die Auswirkungen sehen, die dieses System ausübt. Damit man hier etwas sieht, muss in unserer KomplettVerwalteteKlasse
auch wieder ein Finalizer
überschrieben werden (Den ruft der GC auf). Er ist auskommentiert, da er an dieser Stelle nicht zur Implementation von
IDisposable
gehört (eigentlich wollen wir den gar nicht haben), er wird uns hier auch wieder nur eine Ausgabe bescheren.Der Using-Block um
kvk
vernichtet diese Instanz am Ende. Beim Ablauf des Codes erhalten wir entsprechend die Ausgabe "Disposing". Lassen wir den Using-Block weg und kommentieren den
Finalizer
rein, erhalten wir die Botschaft "Finalizing". Also es ändert sich nur wer aufräumt. Das sieht man auch, wenn man durch den Code stepped: "Disposing" erscheint am Ende des Using-Blocks, "Finalizing" erscheint beim
GC.Collect
Kommando.Also Using wieder rein. Und siehe da: jetzt passiert natürlich beides, "Disposing" und "Finalizing" erscheinen. Das will man nicht, aus Effizienz-Gründen.
Daher wird in die
Dispose
-Methode GC.SuppressFinalize(Me)
eingefügt. Nun wird nur noch Disposed.Warum ist der Finalizer ineffizient? Wir haben in der Betrachtung der Heaps gesehen, das setzen des Heap-Pointer an den Anfang des Heaps erspart enorm viel Arbeit, aber so ein Objekt wie StreamWriter will gelöscht werden. Denn wir können ja nicht warten bis der von neuem Speicherbedarf überschrieben wird. Finalizer stoppen also das generelle Prinzip der schnellen Heap-Bereinigung. Sie reihen sich in eine Finalizing-Warteschlange auch dann, wenn man den
Finalizer
als komplett leere Methode überschreibt.Ein kleines Spielchen noch:
_writer.Dispose
im Finalizer
führt zu einer Exception, denn während der GarbageCollection, in der wir uns beim Finalizen gerade befinden, wurde _writer
bereits vernichtet. Das brauchen wir gleich noch (*)Es gilt in Klassen, deren Bestandteile alle verwaltet sind, braucht man keinen
Finalizer
.Das bringt uns zu Klassen, die nicht-verwaltete Bestandteile enthalten.
---------------------------------------------------------------------------------------------------
Dispose-Pattern:
Wenn wir nicht-verwaltete Ressourcen nutzen, müssen wir sie aufräumen, denn das tun diese genau nicht mehr selbst und dafür bieten sich entsprechende Methodenaufrufe im Finalizer an, denn das ist die Methode die der GC aufruft.
Dadurch kann das System (wenn nötig) automatisch aufräumen. Wir wollen aber auch weiterhin selbst aufräumen.
GC.Collect
könnte man hierfür zum Beispiel halbwegs sinnvoll missbrauchen. Aber verwaltete Ressourcen haben wir höchstwahrscheinlich auch noch, dann geht das schon nicht mehr, weil wir die ja nicht im Finalizer bereinigen können, da dies wie bei (*) gesehen zur Exception führt. Also muss das außerhalb passieren.
Unsere Dispose-Methode muss also:
- verwaltete und nicht verwaltete Ressourcen manuell bereinigbar machen
- nicht verwaltete Ressourcen automatisch bereinigbar machen
- verwaltete Ressourcen nicht versuchen in der automatischen Bereinigung zu bereinigen.
VB.NET-Quellcode
- Imports System.IO
- Imports Excel = Microsoft.Office.Interop.Excel
- Imports System.Runtime.InteropServices
- Class Programm
- Private Shared Sub Run()
- Using gk As GemischteKlasse = New GemischteKlasse()
- gk.StartWriting()
- End Using
- End Sub
- Private Shared Sub Main()
- Run()
- End Sub
- End Class
- Public Class GemischteKlasse
- Implements IDisposable
- Private _writer As StreamWriter
- Private _excel As Excel.Application
- Private disposedValue As Boolean
- Public Sub StartWriting()
- _writer = New StreamWriter("Ausgabe.txt")
- _excel = New Excel.Application()
- End Sub
- Protected Overridable Sub Dispose(ByVal disposing As Boolean)
- If Not disposedValue Then
- If disposing Then
- _writer?.Dispose()
- End If
- If _excel IsNot Nothing Then
- _excel.Quit()
- Marshal.ReleaseComObject(_excel)
- End If
- disposedValue = True
- End If
- End Sub
- Protected Overrides Sub Finalize()
- Dispose(disposing:=False)
- End Sub
- Public Sub Dispose() Implements IDisposable.Dispose
- Dispose(disposing:=True)
- GC.SuppressFinalize(Me)
- End Sub
- End Class
Wenn wir
gk.Dispose
aufrufen, also am Ende des Using-Blocks, dann rufen wir unsere Dispose
Überladung mit Argument True
auf. Dadurch werden- verwaltete und nicht verwaltete Ressourcen manuell bereinigbar gemacht (Sowohl der Code für
Excel
als aus für den StreamWriter
werden hier bearbeitet)Wir überschreiben den Finalizer, dadurch bekommt der GC Zugang zu Methoden, die er sonst nicht hat; also werden die
- nicht verwalteten Ressourcen automatisch bereinigbar gemacht.
Da wir einen Finalizer nun verwenden müssen, im Gegensatz zu Fall 1, wollen wir im echten
Dispose
diesen also unterdrücken (GC.SuppressFinalize(Me)
)Der Finalizer ruft die
Dispose
Überladung mit Argument False
auf und fasst daher nicht den Dispose
von unseren verwalteten Ressourcen an.- es wird nicht versucht verwaltete Ressourcen in der automatischen Bereinigung zu bereinigen
Die Bereinigung von Excel hat spezielle Methoden (
Quit
, ReleaseComObject
) und andere nicht-verwaltete Objekte würden hier dementsprechend andere Methoden benötigen. Die Abfrage von
disposedValue
ermöglicht die Mehrfachausführung der Dispose-Methode sie ist damit idempotent. (Aber nur weil alles gelöscht ist, was gelöscht werden soll bei weiteren Durchläufen; Methoden werden nicht generell durch Abschalten von Mehrfachdurchläufen idempotent)So das ist was ich nun endlich mitnehmen konnte
--------------------------------------------------------------------------------------
Folgende Fehler möchte ich noch anmerken im Beispiel-Code (Ich möchte sie nicht im Code einfach ersetzen, da im Video derselbe "falsche" Code verwendet wird):
Fall2 benötigt in Zeile 36-39 am Ende noch
_excel = Nothing
, sonst würde ein hypothetischer doppelter Finalize
einen Fehler verursachen.Es gibt Fälle in denen
ReleaseComObejct
nicht gut genug ist. Daher weist auch MS darauf hin FinalReleaseComObject
zu verwenden um super sicher zu sein.Danke @ErfinderDesRades
Viele Grüße
Dieser Beitrag wurde bereits 8 mal editiert, zuletzt von „Haudruferzappeltnoch“ ()