Garbage Collection



  • 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 (!) freigegeben
    

    oder 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.



  • Java 1.5?/1.6 und 1.7 verwenden paralleles Generation-GC, sowie .NET seid 2.0 als Standard Garbage Collection. Bei Java kann man sogar den GC über eine Parameter auswechseln, deswegen kann auch eine Kopplung von Referenzzähler in das Objektmodell nicht gegeben sein, oder? In dieser Weiße arbeiten nach meinen Kenntnisstand nur Delphi, Objective C/C++ <= 2.0 und Vala - wobei es durchaus unterschiede gibt. - Evtl. verstehen wir untereinander beim Begriff "Objektmodell" auch etwas anders.

    Die toten Objekte zu erkennen ist im Gegensatz beim Referenzzählung kein triviales Angelegenheit, demnach bei jeden unterschiedlich GC-Konzept auch anders. Mit fehlt gerade auch als Beispiel nur das berühmte "Wagen-Prinzip" ein, dass ein Abgleich zwischen den gefunden Stackpointer mit deren Länge gegen ein Bereich(- welches ein Wagen symbolisiert) in ein anderen Wagen kopiert und den alten Bereich bereinigt - damit sind alle toten Bereiche(und damit auch dessen Objekte) freigibt. Bei diesen Beispiel sieht man, dass das mitführen von Referenzzähler nicht vorgesehen ist, weil nach dem Konzept ein überflüssige Information ist - aber nicht wie in den Sprachen die ich aufgezählt habe.

    GC ist für mich ein zu komplexe Thema um sie alle in eine Schublade zu stecken - immerhin gibst es wissenschaftliche Material zu Nonblocking{lockfree} Real-Time Garbage Collection - zu lesen, hoffentlich find ich die zeit für native nano.



  • Was mir bei Java/C#/... abgeht, ist die Möglichkeit einfach und elegant Shared Ownership von "disposable" Resourcen zu implementieren.
    In C++ geht das ja super-fein mit shared_ptr.

    Natürlich kann man sich in Java/C#/... auch eine Klasse basteln die nen Ref-Count für ein bestimmtes Objekt managt, und dann bei 0 eben Dispose()/close() macht. Nur ist das alles andere als elegant. Und vor allem: es fehlt ein Standard.



  • Michael E. schrieb:

    Zeus: Wie erkennt der GC dann tote Objekte?

    Wie bereits von zeus gesagt, ist das von GC zu GC unterschiedlich.

    Ich weiß, dass der Flash-GC so arbeitet, dass er den Objektgraphen entlangwandert. Das heißt, irgendwo gibt es eine Menge von Objekten, die der GC kennt und er versucht für jedes Objekt zu beweisen, dass es von einem solchen Objekt referenziert wird. Dafür macht er eine Tiefensuche im Graphen und markiert alle Objekte, die referenziert werden. Den Rest löscht er. In der Praxis bricht der GC von Flash leider ab einer bestimmten Iterationstiefe ab und dann gibts memory leaks.

    //edit eine kleine Korrektur...


Anmelden zum Antworten