Garbage Collection
-
HeXor schrieb:
Main function
int main(int argc, char *argv[]) { MyClass obj; obj.foo(); obj.bar(); return 1; }Mach mal:
int main(int argc, char *argv[]) { MyClass obj; MyClass obj2(obj); }Tipp: Du solltest den Kopier- und Zuweisungsoperator implementieren oder unschädlich machen
.
-
HeXor schrieb:
Was mir gerade gekommen ist, ist die Idee das ganze mit dem Destruktor der Klasse zu lösen.
Gratuliere. So steht das in jedem Anfängerbuch genau an der Stelle, an der zum ersten Mal die beiden Begriffe Destruktor und manuelle Speicherverwaltung bekannt sind.
Du kannst das wie Generationen vor dir so machen, das zwar fehleranfällig, aber definitiv besser als gar nichts zu machen. Was die anderen hier dir sagen wollen ist, dass man in der modernen C++-Programmierung nicht mehr in jedem Destruktor allerlei Resourcen freigibt, sondern die Resourcenverwaltung an spezialisierte Klassen delegiert. In dem Fall Smart-Pointer (shared_ptr etc.)
Zwar bedeutet das wieder manuelle GarbageCollection
Das ist, wenn du den Müll rausbringst. In der Programmierung ist Garbage Collection ein anderes Wort für automatische Speicherverwaltung.
-
HeXor schrieb:
Was mir gerade gekommen ist, ist die Idee das ganze mit dem Destruktor der Klasse zu lösen.
Das wurde dir auf Seite 2 Mitte und Seite 3 erster Post bereits gesagt. Aber schön, dass du jetzt endlich anfängst, C++ zu programmieren. Nun musst du nur noch einen Schritt weiter gehen und sagen: "das kann ich automatisieren" und schon bist du bei smart pointern. oder vektoren wenn du Gruppen von Objekten brauchst. Und dann wirst du nie wieder delete selbst schreiben müssen.
-
Vielleicht hilft es, den gedanklichen Vergleich zu modernem Java zu schlagen:
Mit Java 7 wurde das Interface AutoCloseable eingeführt, das eine Möglichkeit bietet, Aufräumarbeiten (wenn auch auf sybtaktisch etwas holprige Weise) zu automatisieren, etwa Dateien oder Sockets zu schließen:
try ( FileInputStream file = new FileInputStream(filename); ) { // ... } // file wird automatisch geschlossen (file.close() aufgerufen)Dieser Mechanismus kommt ursprünglich von C++ und ist da für alle Ressourcenverwaltung üblich, explizit inklusive Speicher. Man braucht kein spezielles Interface und keine spezielle Aufrufssyntax, wo ein Destuktor ist, wird dieser am Ende des Blocks ausgeführt. Das C++-Äquivalent des Java-Beispiels oben wäre
{ std::ifstream file(filename); // ... } // file wird automatisch geschlossen (file.~ifstream() aufgerufen)Und mit Speicher macht man das halt auf die gleiche Weise in verwaltenden Klassen:
{ std::vector<int> v(100); // ... } // v wird hier zerstört, der Speicher sofort (!) freigegebenoder auch
{ std::unique_ptr<some_class> ptr(new some_class(foo, bar)); // ... } // ptr wird hier zerstört, sein Destruktor zerstört in der Folge das Objekt, auf das er zeigt.Soviel zur grundlegenden Idee. Jetzt für Fortgeschrittene: Es ist aus ein paar Gründen sinnvoll, die Speicherverwaltungsaufgaben möglichst eng zu kapseln, also für jede Ressource ein eigenes Verwaltungsobjekt zu haben. Der Grund dafür ist zum einen, dass es übersichtlicher ist, aber auch, dass man unnötige Kopfschmerzen in Eckfällen vermeidet.
Zum Beispiel sieht Folgendes für das ungeübte Auge völlig in Ordnung aus:
class some_class { public: some_class() : p(new some_other_class(1)), q(new some_other_class(2)) { } ~some_class() { delete p; delete q; } private: // Kopiersemantik abschalten. some_class(some_class const &); some_class &operator=(some_class const &); some_other_class *p, *q; };Aber was, wenn some_other_class jetzt beispielsweise so aussieht:
struct some_other_class { some_other_class(int n) { if(n == 2) throw n; } };Rückblick:
some_class() : p(new some_other_class(1)), q(new some_other_class(2)) // <-- das wirft jetzt eine Exception! { }Das some_class-Objekt ist nicht fertig konstruiert, der Destruktor kann also nicht bemüht werden; das ginge auch schief, weil q ins Nirvana zeigt. Zwar werden bereits konstruierte Teilobjekte in so einem Fall vor Verlassen des Konstruktors zerstört, aber nackte Zeiger haben keine Aufräumautomatik. Schon haben wir ein Speicherleck, und das ist gar nicht mal so einfach sinnvoll zu stopfen. Etwas der Form
some_class() try : p(new some_other_class(1)), q(new some_other_class(2)) { } catch(int n) { if(n == 2) delete p; }scheint möglich, aber will man sich das wirklich antun? Und wenn some_class mal weniger vorhersehbar Exceptions wirft, kriegt man noch ganz andere Probleme; so einfach ist nämlich nicht festzustellen, welche Zeiger schon gültige Werte haben.
Vergleiche dagegen das Szenario mit ressourcenverwaltenden Klassen:
class some_class { public: some_class() : p(new some_other_class(1)), q(new some_other_class(2)) { } private: // Kopiersemantik abschalten. some_class(some_class const &); some_class &operator=(some_class const &); std::unique_ptr<some_other_class> p; std::unique_ptr<some_other_class> q; };Hier kann some_other_class::some_other_class Exceptions werfen, wie es lustig ist, der Konstruktor zerstört ggf. die bereits konstruierten Teilobjekte wieder, und diese räumen den angeforderten Kram wieder weg. Merke auch: Ein eigener Destruktor ist hier gar nicht mehr notwendig, weil der generierte alle notwendigen Aufgaben übernimmt.
-
Das neue Resource-try ist doch nur noch eine weitere Verschiebung des Problems. Die sollen mal eine ordentliche, GC-gebundene Lösung bringen.
-
Die gibt es in der JVM schon - wenn der GC ein Objekt aufräumt, wird die spezielle finalize-Methode ausgeführt. Nur bringt der Garbage-Kollektor einen bei den meisten Ressourcen nicht wirklich weiter - wenn ein Socket aus dem Scope geht, will man in aller Regel, dass die Verbindung gleich geschlossen wird und nicht erst, wenn die VM nicht mehr genug Heap-Speicher hat und den GC anschmeißt.
Auch, wenn man die Kriterien für die Collection anders setzt - bei einem GC werden die Aufräumarbeiten generell so lange wie möglich verzögert, weil er anders nicht performant zu betreiben ist. Immerhin hat der Garbage-Collector Data-Races mit allen anderen Threads, und nach derzeitigem Stand der Forschung bedeutet das, dass jeder GC-Lauf ein Stop-the-world-Event beinhaltet. Stell dir ein Client-Server-System vor, in dem beide Seiten ihre Sockets so lang wie möglich am Leben halten, nachdem die Verbindung nicht mehr gebraucht wird.
-
Es gibt aber keinerlei Garantie, dass diese Finalizer jemals aufgerufen werden. Es existiert in Java kein zuverlässiger Mechanismus, um Resourcen garantiert freizugeben. Das Auto-Closeable Dingens ist nur eine weitere Krücke für das sinkende Schiff.
Und wenn der GC so ein großes Problem "Stop-the-world-Event" ist, dann ist Java nicht die Sprache der Wahl für solch ein Client-Server System.
-
Da spricht der Experte.
-
314159265358979 schrieb:
Das neue Resource-try ist doch nur noch eine weitere Verschiebung des Problems. Die sollen mal eine ordentliche, GC-gebundene Lösung bringen.
314159265358979 schrieb:
Und wenn der GC so ein großes Problem "Stop-the-world-Event" ist, dann ist Java nicht die Sprache der Wahl für solch ein Client-Server System.
Das wird die Tomcat-Leute aber überraschen. Man muss, um Java-Programme als Server zu betreiben, ein bisschen mit den JVM-Parametern aufpassen, aber benutzen kann man sie dafür durchaus. Das hängt auch damit zusammen, dass keine "ordentliche, GC-gebundene Lösung" für alle Ressourcentypen verwendet wird.
314159265358979 schrieb:
Es gibt aber keinerlei Garantie, dass diese Finalizer jemals aufgerufen werden. Es existiert in Java kein zuverlässiger Mechanismus, um Resourcen garantiert freizugeben. Das Auto-Closeable Dingens ist nur eine weitere Krücke für das sinkende Schiff.
So ziemlich alle ressourcenverwaltenden Klassen in Java bieten Mechanismen, mit denen man diese wieder freigeben kann; in der Tat würde ich alle, die das nicht tun, als fehlerhaft bezeichnen wollen. Damit existiert ein zuverlässiger Mechanismus, Ressourcen garantiert freizugeben - nur halt vor Java 7 kein komfortabler, und auch in Java 7 keiner, der nachlässige oder schlecht informierte Programmierer davor schützt, Ressourcen zu leaken. Das kann man aber aufgrund (beispielsweise) der Existenz nackter Zeiger auch von C++ nicht behaupten.
Es ist richtig, dass Java in Abwesenheit automatischer Variablen böse Probleme mit der Fehlerbehandlung hatte - es ist nicht untertrieben, Exceptions in diesem Zusammenhang als kaum benutzbar zu bezeichnen - aber die AutoCloseable-Lösung ist eine vertretbare. Vollautomatisches Aufräumen, wie man es in C++ gewohnt ist, wäre in Java selbst dann nicht einführbar, wenn man Bedenken über Rückwärtskompatibilität ignorierte, weil Objekte dort Referenztypen sind und nicht automatisch am Ende des Blocks ihre Gültigkeit verlieren müssen. Eine shared_ptr-artige Vorgehensweise bricht bei Ringstrukturen zusammen, um diesen Einwand gleich abzufangen.
Im Übrigen rate ich dir, dich vor einer Fortsetzung dieser Diskussion über die Funktionsweise generationeller Garbage-Kollektoren zu informieren (diese sind in JVMs üblich).
-
seldon schrieb:
Eine shared_ptr-artige Vorgehensweise bricht bei Ringstrukturen zusammen, um diesen Einwand gleich abzufangen.
Wenn ich Ringkonstrukte hab, dann hab ich sowieso ganz andere, schlimmere Probleme

-
dot schrieb:
seldon schrieb:
Eine shared_ptr-artige Vorgehensweise bricht bei Ringstrukturen zusammen, um diesen Einwand gleich abzufangen.
Wenn ich Ringkonstrukte hab, dann hab ich sowieso ganz andere, schlimmere Probleme

Wie implementierst du eine doppelt verkettete Liste?
-
Michael E. schrieb:
dot schrieb:
seldon schrieb:
Eine shared_ptr-artige Vorgehensweise bricht bei Ringstrukturen zusammen, um diesen Einwand gleich abzufangen.
Wenn ich Ringkonstrukte hab, dann hab ich sowieso ganz andere, schlimmere Probleme

Wie implementierst du eine doppelt verkettete Liste?
Nicht mit zirkulären shared_ptr!?
Sowas wäre imo ein schwerer Designfehler. Was macht es für einen Sinn dass sich Listenelemente gegenseitig besitzen? Ihre Lebensdauer ist im Allgemeinen völlig unabhängig. Sofern es sich nicht um eine ganz merkwürdige Art von Liste handelt, klingt mir Reference Counting da von vornherein nach einer sehr schlechten Idee.
Aber zum Glück ist die Welt ja bei shared_ptr noch lange nicht zu Ende.
Zuallermindest gibt es weak_ptr um solche Beziehungen aufzulösen...
-
dot schrieb:
Nicht mit zirkulären shared_ptr!?
Sowas wäre imo ein schwerer Designfehler. Was macht es für einen Sinn dass sich Listenelemente gegenseitig besitzen? Ihre Lebensdauer ist im Allgemeinen völlig unabhängig. Sofern es sich nicht um eine ganz merkwürdige Art von Liste handelt, klingt mir Reference Counting da von vornherein nach einer sehr schlechten Idee.
Aber zum Glück ist die Welt ja bei shared_ptr noch lange nicht zu Ende.
Zuallermindest gibt es weak_ptr um solche Beziehungen aufzulösen...Und jetzt erklär mir bitte noch, wie der GC entscheiden soll, ob er weak_ptr oder shared_ptr benutzen soll

Deine Erklärung, warum shared_ptr für doppelt verkettete Listen Quatsch ist, ist richtig. Trotzdem wirst du wohl nicht behaupten, dass doppelt verkttete Listen mit ihren zirkulären Pointern Quatsch sind. Also funktioniert ein GC wohl nicht mit shared_ptr. Nichts anderes wurde behauptet.
-
Wir reden hier ja über ein Objektsystem, wie es Java besitzt, also Klassen als Referenztypen. Alle Java-Objekte haben Referenzzähler, aber diese alleine reichen nicht aus, um in allen Strukturen den tatsächlichen Todeszeitpunkt eines Objektes zu bemerken. Stell dir vor, du hättest keine nackten Zeiger, sondern nur shared_ptrs - wie baust du eine doppelt verkettete Liste und verhinderst Speicherlecks? Wie Michael richtig bemerkt, enthalten doppelt verkettete Listen massig Ringstrukturen im relevanten Sinn, und die muss man unter diesen Bedingungen anders abkanzeln.
Java und die meisten (wenn nicht alle) anderen Sprachen mit diesem Problem lösen das durch einen Garbage-Kollektor, der alle Weile mal kuckt, ob die Objekte noch gebraucht werden. Wenn man sich auf den Standpunkt stellen kann, dass diese Verzögerung bei Speicher kein Problem darstellt (was häufig der Fall ist), funktioniert das da auch ganz gut, aber eine allgemeine Ressourcenverwaltung kann man auf diese Weise halt nicht betreiben.
-
Darum gings doch nie!? Es ging darum zu zeigen, dass es nix ausmacht dass eine shared_ptr Lösung nicht mit zirkulären Abhängigkeiten klarkommt weil das sowieso ein Designfehler wäre.
Dass C++ nicht Java ist, ist mir klar und ich bin jeden Tag aufs Neue froh drüber
-
dot schrieb:
Darum gings doch nie!?
Eigentlich schon :p Keiner bezweifelt den Sinn von shared_ptr in C++ und jeder weiß, welche Alternativen es gibt. Aber seldon spricht doch über GC-Strategien und dass es keinen Sinn macht, dass ein GC sich auf shared_ptr stützt.
-
Dann hab ich seldons Post falsch verstanden

-
seldon schrieb:
Wir reden hier ja über ein Objektsystem, wie es Java besitzt, also Klassen als Referenztypen. Alle Java-Objekte haben Referenzzähler, aber ...
Ich hoffe dies ist ein Gedankenexperiment, denn dem ist nicht so...
-
Zeus: Wie erkennt der GC dann tote Objekte?
-
Ach, ich Depp. Du hast natürlich Recht, Hotspot betreibt mark-and-sweep über Eden und die Survivor-Spaces (richtig?). Ich bin da gehörig durcheinandergeraten, tut mir Leid.
Also, auf ein neues: Javas Objektsystem wäre durch Referenzzählung allein nicht darstellbar. Letztendlich besteht das Problem, dass der Zeitpunkt, wann ein Objekt nicht mehr gebraucht wird, in diesem System nicht trivial bestimmbar ist, und der Rest der Argumentation gegen eine GC-gestützte, allgemeine Ressourcenverwaltung bleibt unverändert.