C ++ löschen gegen Java GC

8

Die Java-Garbage Collection kümmert sich um tote Objekte auf dem Heap, friert aber manchmal die Welt ein. In C ++ muss ich aufrufen delete, um ein erstelltes Objekt am Ende seines Lebenszyklus zu entsorgen.

Dies deletescheint ein sehr niedriger Preis für eine nicht gefrierende Umgebung zu sein. Das Platzieren aller relevanten deleteSchlüsselwörter ist eine mechanische Aufgabe. Man kann ein Skript schreiben, das den Code durchläuft und Löschen platziert, sobald keine neuen Zweige ein bestimmtes Objekt verwenden.

Also, was sind die Vor- und Nachteile von Java in vs C ++ DIY-Modell der Garbage Collection.


Ich möchte keinen C ++ vs Java-Thread starten. Meine Frage ist anders.
Diese ganze GC-Sache - läuft es darauf hinaus, "nur ordentlich zu sein, nicht zu vergessen, von Ihnen erstellte Objekte zu löschen - und Sie benötigen keine dedizierte GC? Oder ist es eher so, als ob" das Entsorgen von Objekten in C ++ wirklich schwierig ist - ich verbringe 20% meiner Zeit damit und doch sind Speicherlecks ein häufiger Ort "?

Sechzig Bäume
quelle
15
Es fällt mir schwer zu glauben, dass Ihr magisches Skript existieren könnte. Abgesehen von allem müsste es keine Lösung für das Halteproblem enthalten. Oder alternativ (und wahrscheinlicher) nur für unglaublich einfache Programme arbeiten
Richard Tingle
1
Auch modernes Java friert nur unter extremen Bedingungen ein, wo große Mengen Müll entstehen
Richard Tingle
1
@ThomasCarlisle gut, es kann - und tut es gelegentlich - die Leistung beeinflussen. In diesen Fällen müssen jedoch viele Parameter angepasst werden, und manchmal besteht die Lösung darin, auf einen anderen gc zu wechseln. Es hängt alles von der Menge der verfügbaren Ressourcen und der typischen Last ab.
Hulk
4
" Das Platzieren aller relevanten Löschschlüsselwörter ist eine mechanische Aufgabe " - Deshalb gibt es Tools zum Erkennen von Speicherlecks. Weil es so einfach und überhaupt nicht fehleranfällig ist.
JensG
2
@Doval, wenn Sie RAII richtig verwenden, was ziemlich einfach ist, gibt es fast überhaupt keine Buchhaltung. newgeht in den Konstruktor Ihrer Klasse, der den Speicher verwaltet, deletegeht in den Destruktor. Von dort ist alles automatisch (Speicherung). Beachten Sie, dass dies im Gegensatz zur Speicherbereinigung für alle Arten von Ressourcen funktioniert, nicht nur für den Speicher. Mutexe werden in einem Konstruktor aufgenommen und in einem Destruktor freigegeben. Dateien werden in einem Konstruktor geöffnet und in einem Destruktor geschlossen.
Rob K

Antworten:

18

Der C ++ - Objektlebenszyklus

Wenn Sie lokale Objekte erstellen, müssen Sie diese nicht löschen: Der Compiler generiert Code, um sie automatisch zu löschen, wenn das Objekt den Gültigkeitsbereich verlässt

Wenn Sie Objektzeiger verwenden und Objekte im freien Speicher erstellen, müssen Sie darauf achten, das Objekt zu löschen, wenn es nicht mehr benötigt wird (wie Sie beschrieben haben). Leider kann dies in komplexer Software viel schwieriger sein, als es aussieht (z. B. was ist, wenn eine Ausnahme ausgelöst wird und der erwartete Löschteil nie erreicht wird?).

Glücklicherweise haben Sie in modernem C ++ (auch bekannt als C ++ 11 und höher) intelligente Zeiger, wie zum Beispiel shared_ptr. Sie zählen das erstellte Objekt auf sehr effiziente Weise, ähnlich wie es ein Garbage Collector in Java tun würde. Und sobald nicht mehr auf das Objekt verwiesen wird, shared_ptrlöscht der zuletzt aktive das Objekt für Sie. Automatisch. Wie Garbage Collector, jedoch jeweils ein Objekt und ohne Verzögerung (Ok: Sie benötigen besondere Sorgfalt und müssen weak_ptrmit Zirkelverweisen umgehen).

Fazit: Sie können heutzutage C ++ - Code schreiben, ohne sich um die Speicherzuordnung kümmern zu müssen. Dieser Code ist so leckagefrei wie bei einem GC, jedoch ohne den Freeze-Effekt.

Der Java-Objektlebenszyklus

Das Schöne ist, dass Sie sich keine Gedanken über den Objektlebenszyklus machen müssen. Sie erstellen sie einfach und Java kümmert sich um den Rest. Ein moderner GC identifiziert und die Objekte zerstören , die nicht mehr benötigt werden (einschließlich , wenn es zirkuläre Verweise zwischen toten Objekten).

Aufgrund dieses Komforts haben Sie leider keine wirkliche Kontrolle darüber, wann das Objekt wirklich gelöscht wird . Semantisch fällt die Löschung / Zerstörung mit der Speicherbereinigung zusammen .

Dies ist vollkommen in Ordnung, wenn Objekte nur in Bezug auf den Speicher betrachtet werden. Mit Ausnahme des Einfrierens, aber dies ist kein Todesfall (die Leute arbeiten daran). Ich bin kein Java-Experte, aber ich denke, dass die verzögerte Zerstörung es schwieriger macht, Lecks in Java zu identifizieren , da Referenzen versehentlich aufbewahrt werden, obwohl Objekte nicht mehr benötigt werden (dh Sie können das Löschen von Objekten nicht wirklich überwachen).

Was aber, wenn das Objekt andere Ressourcen als den Speicher steuern muss, z. B. eine geöffnete Datei, ein Semaphor oder einen Systemdienst? Ihre Klasse muss eine Methode zum Freigeben dieser Ressourcen bereitstellen. Und Sie müssen sicherstellen, dass diese Methode aufgerufen wird, wenn die Ressourcen nicht mehr benötigt werden. Stellen Sie in jedem möglichen Verzweigungspfad durch Ihren Code sicher, dass er auch bei Ausnahmen aufgerufen wird. Die Herausforderung ist dem expliziten Löschen in C ++ sehr ähnlich.

Schlussfolgerung: Der GC löst ein Speicherverwaltungsproblem. Die Verwaltung anderer Systemressourcen wird jedoch nicht behandelt. Das Fehlen einer "Just-in-Time" -Löschung kann das Ressourcenmanagement sehr schwierig machen.

Löschen, Speicherbereinigung und RAII

Wenn Sie das Löschen eines Objekts und den Destruktor steuern können, der beim Löschen aufgerufen werden soll, können Sie den RAII nutzen . Bei diesem Ansatz wird der Speicher nur als Sonderfall der Ressourcenzuweisung betrachtet und die Ressourcenverwaltung sicherer mit dem Objektlebenszyklus verknüpft, wodurch eine streng kontrollierte Ressourcennutzung sichergestellt wird.

Christophe
quelle
3
Das Schöne an (modernen) Müllsammlern ist, dass Sie nicht wirklich über zyklische Referenzen nachdenken müssen. Solche Gruppen von Objekten, die nur voneinander erreichbar sind, werden erkannt und gesammelt. Dies ist ein großer Vorteil gegenüber einfachen Referenzzählern / intelligenten Zeigern.
Hulk
6
+1 Der Teil "Ausnahmen" kann nicht genug betont werden. Das Vorhandensein von Ausnahmen macht die schwierige und sinnlose Aufgabe der manuellen Speicherverwaltung praktisch unmöglich, und daher wird die manuelle Speicherverwaltung in C ++ nicht verwendet. Verwenden Sie RAII. Verwenden Sie nicht newaußerhalb von Konstruktoren / intelligenten Zeigern und niemals deleteaußerhalb eines Destruktors.
Felix Dombek
@Hulk Du hast hier einen Punkt! Obwohl die meisten meiner Argumente immer noch gültig sind, hat GC große Fortschritte gemacht. Und Zirkelreferenzen sind in der Tat schwierig zu handhaben, wenn nur intelligente Zeiger mit Referenzzählung verwendet werden. Deshalb habe ich meine Antwort entsprechend bearbeitet, um ein besseres Gleichgewicht zu halten. Ich habe auch einen Verweis auf einen Artikel über mögliche Strategien zur Abschwächung des Einfriereffekts hinzugefügt.
Christophe
3
+1 Die Verwaltung anderer Ressourcen als des Speichers erfordert in der Tat einen zusätzlichen Aufwand für GC-Sprachen, da RAII nicht funktioniert, wenn keine genaue Kontrolle darüber besteht, wann (oder ob) das Löschen / Finalisieren eines Objekts erfolgt. In den meisten dieser Sprachen sind spezielle Konstrukte verfügbar (siehe beispielsweise Java's Try-with-Resources ), die in Kombination mit Compiler-Warnungen dazu beitragen können, diese Dinge richtig zu machen.
Hulk
1
Like garbage collector, but one object at a time and without delay (Ok: you need some extra care and weak_ptr to cope with circular references).Die Referenzzählung kann jedoch kaskadieren. ZB Der letzte Verweis auf Ageht weg, hat aber Aauch den letzten Verweis auf B, wer den letzten Verweis auf C... And you'll have the responsibility to make sure that this method is called when the resources are no longer needed. In every possible branching path through your code, ensuring it is also invoked in case of exceptions.Java und C # haben spezielle Blockanweisungen dafür.
Doval
5

Diese Löschung scheint ein sehr niedriger Preis für eine nicht einfrierende Umgebung zu sein. Das Platzieren aller relevanten Löschschlüsselwörter ist eine mechanische Aufgabe. Man kann ein Skript schreiben, das den Code durchläuft und Löschen platziert, sobald keine neuen Zweige ein bestimmtes Objekt verwenden.

Wenn Sie so ein Skript schreiben können, herzlichen Glückwunsch. Du bist ein besserer Entwickler als ich. Bei weitem.

Die einzige Möglichkeit, Speicherlecks in praktischen Fällen tatsächlich zu vermeiden, sind sehr strenge Codierungsstandards mit sehr strengen Regeln, wer der Eigentümer eines Objekts ist und wann es freigegeben werden kann und muss, oder Tools wie intelligente Zeiger, die Verweise auf Objekte zählen und löschen Objekte, wenn die letzte Referenz weg ist.

gnasher729
quelle
4
Ja, die manuelle Ressourcenverwaltung ist immer fehleranfällig. Glücklicherweise ist sie nur in Java (für Nicht-Speicherressourcen) erforderlich, da C ++ über RAII verfügt.
Deduplikator
Der einzige Weg, wie Sie in praktischen Fällen tatsächlich Ressourcenlecks vermeiden können, sind sehr strenge Codierungsstandards mit sehr strengen Regeln, wer der Eigentümer eines Objekts ist ... Speicher ist nicht die einzige Ressource und in den meisten Fällen nicht die kritischste entweder.
Neugieriger
2
Wenn Sie RAII als "sehr strengen Codierungsstandard" betrachten. Ich halte es für eine "Grube des Erfolgs", die trivial einfach zu bedienen ist.
Rob K
5

Wenn Sie mit RAII korrekten C ++ - Code schreiben, schreiben Sie normalerweise keinen neuen oder löschen. Das einzige "Neue", das Sie schreiben, befindet sich in gemeinsam genutzten Zeigern, sodass Sie "Löschen" wirklich nie verwenden müssen.

Nikko
quelle
1
Sie sollten überhaupt nicht verwenden new, auch nicht mit gemeinsam genutzten Zeigern - Sie sollten std::make_sharedstattdessen verwenden.
Jules
1
oder besser , make_unique. Es ist wirklich sehr selten, dass Sie tatsächlich eine gemeinsame Eigentümerschaft benötigen.
Marc
4

Das Leben von Programmierern zu erleichtern und Speicherlecks zu verhindern, ist ein wichtiger Vorteil der Speicherbereinigung, aber nicht nur einer. Ein weiterer Grund ist die Verhinderung der Speicherfragmentierung. Wenn Sie in C ++ ein Objekt mit dem newSchlüsselwort zuweisen , bleibt es an einer festen Position im Speicher. Dies bedeutet, dass Sie während der Ausführung der Anwendung Lücken im freien Speicher zwischen den zugewiesenen Objekten haben. Das Zuweisen von Speicher in C ++ muss daher notwendigerweise ein komplizierterer Prozess sein, da das Betriebssystem in der Lage sein muss, nicht zugewiesene Blöcke mit einer bestimmten Größe zu finden, die zwischen die Lücken passen.

Die Speicherbereinigung kümmert sich darum, indem alle nicht gelöschten Objekte in den Speicher verschoben werden, sodass sie einen fortlaufenden Block bilden. Wenn Sie feststellen, dass die Speicherbereinigung einige Zeit in Anspruch nimmt, liegt dies wahrscheinlich an diesem Prozess und nicht an der Freigabe des Speichers. Der Vorteil davon ist, dass die Speicherzuweisung fast so einfach ist wie das Verschieben eines Zeigers auf das Ende des Stapels.

In C ++ ist das Löschen von Objekten also schnell, das Erstellen kann jedoch langsam sein. In Java dauert das Erstellen von Objekten überhaupt keine Zeit, aber Sie müssen ab und zu einige Reinigungsarbeiten durchführen.

Kamilk
quelle
4
Ja, die Zuweisung aus dem freien Speicher ist in C ++ langsamer als in Java. Glücklicherweise ist es weitaus seltener und Sie können nach eigenem Ermessen problemlos Allokatoren verwenden, wenn Sie ungewöhnliche Muster haben. Außerdem sind alle Ressourcen in C ++ gleich, Java hat die Sonderfälle.
Deduplikator
3
In 20 Jahren Codierung in C ++ habe ich nie gesehen, dass Speicherfragmentierung zu einem Problem wurde. Moderne Betriebssysteme mit virtueller Speicherverwaltung auf Prozessoren mit mehreren Cache-Ebenen haben dieses Problem weitgehend behoben.
Rob K
@RobK - "Moderne Betriebssysteme mit virtueller Speicherverwaltung auf Prozessoren mit mehreren Cache-Ebenen haben das Problem der [Fragmentierung] weitgehend beseitigt" - nein, haben sie nicht. Sie werden es vielleicht nicht bemerken, aber es passiert immer noch, es verursacht immer noch (1) Speicherverschwendung und (2) weniger effiziente Verwendung von Caches, und die einzig praktikablen Lösungen sind entweder das Kopieren von GC oder ein sehr sorgfältiges Design der manuellen Speicherverwaltung, um sicherzustellen, dass dies nicht der Fall ist nicht passieren.
Jules
3

Javas Hauptversprechen waren

  1. Verständliche C-ähnliche Syntax
  2. Schreibe überall einen Lauf
  3. Wir erleichtern Ihnen die Arbeit - wir kümmern uns sogar um den Müll.

Es scheint, als würde Java Ihnen garantieren, dass der Müll entsorgt wird (nicht unbedingt auf effiziente Weise). Wenn Sie C / C ++ verwenden, haben Sie sowohl Freiheit als auch Verantwortung. Sie können es besser machen als Javas GC, oder Sie können viel schlechter sein ( deletealles zusammen überspringen und Probleme mit Speicherverlusten haben).

Wenn Sie Code benötigen, der "bestimmte Qualitätsstandards erfüllt" und das "Preis- / Qualitätsverhältnis" optimiert, verwenden Sie Java. Wenn Sie bereit sind, zusätzliche Ressourcen (Zeit Ihrer Experten) zu investieren, um die Leistung unternehmenskritischer Anwendungen zu verbessern, verwenden Sie C.

Ju Shua
quelle
Nun, alle Versprechen können leicht als gebrochen angesehen werden.
Deduplikator
Abgesehen davon, dass der einzige Müll, den Java sammelt, dynamisch zugewiesener Speicher ist. Es wird nichts gegen andere dynamisch zugewiesene Ressourcen unternommen.
Rob K
@RobK - das stimmt nicht ganz. Die Verwendung von Objekt-Finalisierern kann die Freigabe anderer Ressourcen handhaben. Es wird allgemein davon abgeraten, weil Sie dies in den meisten Fällen nicht möchten (im Gegensatz zum Speicher sind die meisten anderen Ressourcentypen viel eingeschränkter oder sogar eindeutiger, und daher ist eine genaue Freigabe von entscheidender Bedeutung), aber es kann getan werden. Java verfügt auch über die Try-with-Resources-Anweisung, mit der die Verwaltung anderer Ressourcen automatisiert werden kann (mit ähnlichen Vorteilen wie bei RAII).
Jules
@Jules: Ein wesentlicher Unterschied besteht darin, dass sich C ++ - Objekte selbst zerstören. In Java können sich schließbare Objekte nicht selbst schließen. Ein Java-Entwickler, der "new Something (...)" oder (schlechter) Something s = SomeFactory.create (...) schreibt, muss prüfen, ob etwas geschlossen werden kann. Das Ändern einer Klasse von nicht schließbar zu schließbar ist die schlimmste Art der Änderung und kann daher praktisch nie durchgeführt werden. Auf Klassenebene nicht so schlimm, aber ein ernstes Problem, wenn man eine Java-Schnittstelle definiert.
Kevin Cline
2

Der große Unterschied, den die Garbage Collection macht, besteht nicht darin, dass Sie Objekte nicht explizit löschen müssen. Der viel größere Unterschied besteht darin, dass Sie keine Objekte kopieren müssen .

Dies hat Auswirkungen, die beim Entwerfen von Programmen und Schnittstellen im Allgemeinen allgegenwärtig werden. Lassen Sie mich nur ein kleines Beispiel geben, um zu zeigen, wie weitreichend dies ist.

Wenn Sie in Java etwas von einem Stapel entfernen, wird der Wert, der angezeigt wird, zurückgegeben, sodass Sie folgenden Code erhalten:

WhateverType value = myStack.Pop();

In Java ist dies ausnahmesicher, da wir nur einen Verweis auf ein Objekt kopieren, was garantiert ohne Ausnahme geschieht. Das gleiche gilt jedoch nicht für C ++. In C ++ bedeutet das Zurückgeben eines Werts , dass dieser Wert kopiert wird (oder zumindest bedeuten kann ), und bei einigen Typen, die eine Ausnahme auslösen könnten. Wenn die Ausnahme ausgelöst wird, nachdem der Artikel vom Stapel entfernt wurde, aber bevor die Kopie zum Empfänger gelangt, ist der Artikel durchgesickert. Um dies zu verhindern, verwendet der Stapel von C ++ einen etwas ungeschickteren Ansatz, bei dem das Abrufen des obersten Elements und das Entfernen des obersten Elements zwei separate Vorgänge sind:

WhateverType value = myStack.top();
myStack.pop();

Wenn die erste Anweisung eine Ausnahme auslöst, wird die zweite nicht ausgeführt. Wenn also beim Kopieren eine Ausnahme ausgelöst wird, bleibt das Element auf dem Stapel, als wäre überhaupt nichts passiert.

Das offensichtliche Problem ist, dass dies einfach ungeschickt und (für Leute, die es nicht benutzt haben) unerwartet ist.

Dasselbe gilt für viele andere Teile von C ++: Insbesondere für generischen Code durchdringt die Ausnahmesicherheit viele Teile des Designs - und dies ist zum großen Teil auf die Tatsache zurückzuführen, dass die meisten zumindest potenziell das Kopieren von Objekten (die möglicherweise werfen) beinhalten, wo Java würde nur neue Verweise auf vorhandene Objekte erstellen (die nicht geworfen werden können, sodass wir uns keine Gedanken über Ausnahmen machen müssen).

Soweit ein einfaches Skript einzufügen , deletewo nötig: Wenn Sie statisch bestimmen können , wenn Elemente auf der Grundlage der Struktur des Quellcodes zu löschen, soll es wahrscheinlich nicht wurde mit newund deletean erster Stelle.

Lassen Sie mich ein Beispiel für ein Programm geben, für das dies mit ziemlicher Sicherheit nicht möglich wäre: ein System zum Tätigen, Verfolgen, Abrechnen (usw.) von Telefonanrufen. Wenn Sie Ihr Telefon wählen, wird ein "Anruf" -Objekt erstellt. Das Anrufobjekt verfolgt, wen Sie angerufen haben, wie lange Sie mit ihnen gesprochen haben usw., um den Abrechnungsprotokollen entsprechende Datensätze hinzuzufügen. Das Aufrufobjekt überwacht den Hardwarestatus. Wenn Sie also auflegen, zerstört es sich selbst (unter Verwendung der allgemein diskutierten delete this;). Nur ist es nicht so trivial wie "wenn du auflegst". Sie können beispielsweise eine Telefonkonferenz einleiten, zwei Personen verbinden und auflegen. Der Anruf wird jedoch auch nach dem Auflegen zwischen diesen beiden Teilnehmern fortgesetzt (die Abrechnung kann sich jedoch ändern).

Jerry Sarg
quelle
"Wenn die Ausnahme ausgelöst wird, nachdem der Artikel vom Stapel entfernt wurde, aber bevor die Kopie zum Empfänger gelangt, ist der Artikel durchgesickert." Haben Sie Referenzen dafür? Weil das extrem komisch klingt, obwohl ich kein C ++ - Experte bin.
Esben Skov Pedersen
@EsbenSkovPedersen: GoTW # 8 wäre ein vernünftiger Ausgangspunkt. Wenn Sie Zugriff darauf haben, bietet Exceptional C ++ einiges mehr. Beachten Sie, dass beide mindestens einige bereits vorhandene Kenntnisse in C ++ erwarten .
Jerry Coffin
Das scheint einfach zu sein. Es ist tatsächlich dieser Satz, der mich verwirrte: "In C ++ bedeutet das Zurückgeben eines Wertes (oder kann zumindest bedeuten) das Kopieren dieses Werts und bei einigen Typen, die eine Ausnahme auslösen könnten." Befindet sich diese Kopie auf dem Heap oder Stack?
Esben Skov Pedersen
Sie müssen absolut keine Objekte in C ++ kopieren, aber Sie können, wenn Sie möchten, im Gegensatz zu den durch Müll gesammelten Sprachen (Java, C #), die es zu einer PITA machen, ein Objekt zu kopieren, wenn Sie möchten. 90% der von mir erstellten Objekte sollten zerstört und ihre Ressourcen freigegeben werden, wenn sie den Gültigkeitsbereich verlassen. Um alle Objekte in einen dynamischen Speicher zu zwingen, weil 10% von ihnen bestenfalls dumm erscheinen müssen.
Rob K
2
Dies ist jedoch kein wirklicher Unterschied, der durch die Verwendung der Garbage Collection verursacht wird, sondern durch Javas vereinfachte Philosophie "Alle Objekte sind Referenzen". Betrachten Sie C # als Gegenbeispiel: Es handelt sich um eine durch Müll gesammelte Sprache, die jedoch auch Wertobjekte ("Strukturen" in der lokalen Terminologie, die sich von C ++ - Strukturen unterscheiden) mit Kopiersemantik enthält. C # vermeidet das Problem, indem (1) eine klare Trennung zwischen Referenz- und Werttypen besteht und (2) Werttypen immer mit Byte-für-Byte-Kopieren und nicht mit Benutzercode kopiert werden, wodurch Ausnahmen beim Kopieren vermieden werden.
Jules
2

Etwas, von dem ich glaube, dass es hier nicht erwähnt wurde, ist, dass es Effizienzvorteile gibt, die durch die Speicherbereinigung entstehen. In den am häufigsten verwendeten Java-Kollektoren ist der Hauptort, an dem Objekte zugewiesen werden, ein Bereich, der für einen Kopierkollektor reserviert ist. Wenn die Dinge beginnen, ist dieser Raum leer. Wenn Objekte erstellt werden, werden sie im großen offenen Bereich nebeneinander zugewiesen, bis im verbleibenden zusammenhängenden Bereich kein Objekt mehr zugewiesen werden kann. Der GC tritt ein und sucht nach Objekten in diesem Raum, die nicht tot sind. Es kopiert die lebenden Objekte in einen anderen Bereich und fügt sie zusammen (dh keine Fragmentierung). Der alte Raum wird als sauber betrachtet. Anschließend werden die Objekte weiterhin eng miteinander zugeordnet, und dieser Vorgang wird nach Bedarf wiederholt.

Dies hat zwei Vorteile. Das erste ist, dass keine Zeit für das Löschen der nicht verwendeten Objekte aufgewendet wird. Sobald die lebenden Objekte kopiert wurden, gilt der Schiefer als sauber und die toten Objekte werden einfach vergessen. In vielen Anwendungen leben die meisten Objekte nicht sehr lange, sodass die Kosten für das Kopieren des Live-Sets im Vergleich zu Einsparungen, die dadurch erzielt werden, dass Sie sich keine Sorgen um das Dead-Set machen müssen, günstig sind.

Der zweite Vorteil besteht darin, dass beim Zuweisen eines neuen Objekts nicht nach einem zusammenhängenden Bereich gesucht werden muss. Die VM weiß immer, wo das nächste Objekt platziert werden soll (Einschränkung: Vereinfachtes Ignorieren der Parallelität).

Diese Art der Sammlung und Zuordnung ist sehr schnell. Aus Sicht des Gesamtdurchsatzes ist es für viele Szenarien schwer zu übertreffen. Das Problem ist, dass einige Objekte länger leben, als Sie sie weiter kopieren möchten. Dies bedeutet letztendlich, dass der Kollektor von Zeit zu Zeit eine erhebliche Zeitspanne einlegen muss und wann dies passieren kann, kann unvorhersehbar sein. Abhängig von der Länge der Pause und der Art der Anwendung kann dies ein Problem sein oder auch nicht. Es gibt mindestens einen pausenlosen Sammler. Ich gehe davon aus, dass es einen Kompromiss zwischen geringerer Effizienz gibt, um die pausenlose Natur zu erreichen, aber einer der Gründer dieses Unternehmens (Gil Tene) ist ein Überexperte bei GC, und seine Präsentationen sind eine großartige Informationsquelle über GC.

JimmyJames
quelle
Azul wurde von einer Gruppe von GC-Leuten, Compilern, Performance-Leuten und ehemaligen Java-CPU-Leuten von Sun gegründet. Sie wissen, was sie tun.
Jörg W Mittag
@ JörgWMittag aktualisiert, um zu reflektieren, dass es mehr als einen Gründer gab. danke
JimmyJames
1

Oder ist es eher so, als ob "das Entsorgen von Objekten in C ++ wirklich schwierig ist - ich verbringe 20% meiner Zeit damit und dennoch sind Speicherlecks ein häufiger Ort"?

Nach meiner persönlichen Erfahrung in C ++ und sogar C waren Speicherlecks nie ein großer Kampf, den man vermeiden konnte. Mit einem vernünftigen Testverfahren und Valgrind zum Beispiel wird jedes physische Leck, das durch einen Anruf operator new/mallocohne entsprechende verursacht delete/freewird, oft schnell erkannt und behoben. Um fair zu sein, können einige große C ++ - oder C ++ - Codebasen der alten Schule sehr wahrscheinlich einige undurchsichtige Randfälle aufweisen, die hier und da einige Bytes Speicher physisch verlieren können, da dies nicht der deleting/freeingFall ist, der unter dem Radar des Testens geflogen ist.

Was die praktischen Beobachtungen angeht, sind die undichtesten Anwendungen, auf die ich stoße (wie bei Anwendungen, die immer mehr Speicher verbrauchen, je länger Sie sie ausführen, obwohl die Datenmenge, mit der wir arbeiten, nicht wächst), normalerweise nicht in C oder geschrieben C ++. Ich finde keine Dinge wie den Linux-Kernel oder die Unreal Engine oder sogar den nativen Code, der zur Implementierung von Java verwendet wird, in der Liste der undichten Software, auf die ich stoße.

Die bekannteste Art von undichter Software, auf die ich stoße, sind Dinge wie Flash-Applets, wie Flash-Spiele, obwohl sie die Speicherbereinigung verwenden. Und das ist kein fairer Vergleich, wenn man daraus etwas ableiten sollte, da viele Flash-Anwendungen von angehenden Entwicklern geschrieben wurden, denen wahrscheinlich solide technische Prinzipien und Testverfahren fehlen (und ich bin mir auch sicher, dass es qualifizierte Fachleute gibt, die mit GC arbeiten Kämpfe nicht mit undichter Software), aber ich würde jedem viel zu sagen haben, der glaubt, dass GC das Schreiben von undichter Software verhindert.

Baumelnde Zeiger

Aus meiner speziellen Domäne, Erfahrung und da ich hauptsächlich C und C ++ verwende (und ich gehe davon aus, dass die Vorteile von GC je nach unseren Erfahrungen und Bedürfnissen variieren werden), ist das Sofortigste, was GC für mich löst, nicht das Problem des praktischen Speicherverlusts baumelnder Zeigerzugriff, und das könnte in geschäftskritischen Szenarien buchstäblich ein Lebensretter sein.

Leider ersetzt GC in vielen Fällen, in denen ein ansonsten baumelnder Zeigerzugriff gelöst wird, dieselbe Art von Programmiererfehler durch einen logischen Speicherverlust.

Wenn Sie sich vorstellen, dass ein Flash-Spiel von einem angehenden Programmierer geschrieben wurde, speichert er möglicherweise Verweise auf Spielelemente in mehreren Datenstrukturen, sodass diese gemeinsam Eigentümer dieser Spielressourcen sind. Nehmen wir leider an, er macht einen Fehler, bei dem er vergessen hat, die Spielelemente aus einer der Datenstrukturen zu entfernen, als er zur nächsten Stufe überging, um zu verhindern, dass sie freigegeben werden, bis das gesamte Spiel beendet ist. Das Spiel scheint jedoch immer noch gut zu funktionieren, da die Elemente nicht gezeichnet werden oder die Benutzerinteraktion beeinträchtigen. Trotzdem verbraucht das Spiel immer mehr Speicher, während sich die Frameraten selbst zu einer Diashow entwickeln, während die versteckte Verarbeitung immer noch diese versteckte Sammlung von Elementen im Spiel durchläuft (deren Größe inzwischen explosiv geworden ist). Dies ist das Problem, auf das ich in solchen Flash-Spielen häufig stoße.

  • Ich habe Leute getroffen, die sagten, dass dies nicht als "Speicherverlust" gilt, da der Speicher beim Schließen der Anwendung immer noch freigegeben wird und stattdessen als "Speicherverlust" oder ähnliches bezeichnet werden könnte. Während eine solche Unterscheidung nützlich sein könnte, um Probleme zu identifizieren und darüber zu sprechen, finde ich solche Unterscheidungen in diesem Zusammenhang nicht so nützlich, wenn wir darüber sprechen, als wäre sie nicht so problematisch wie ein "Speicherverlust", wenn wir es tun Das praktische Ziel, sicherzustellen, dass die Software nicht so viel Speicherplatz beansprucht, je länger wir sie ausführen (es sei denn, es handelt sich um obskure Betriebssysteme, die den Speicher eines Prozesses beim Beenden nicht freigeben).

Nehmen wir nun an, derselbe angehende Entwickler hat das Spiel in C ++ geschrieben. In diesem Fall gibt es normalerweise nur eine zentrale Datenstruktur im Spiel, die den Speicher "besitzt", während andere auf diesen Speicher verweisen. Wenn er den gleichen Fehler macht, besteht die Möglichkeit, dass das Spiel beim Übergang zur nächsten Stufe aufgrund des Zugriffs auf baumelnde Zeiger abstürzt (oder schlimmer noch, etwas anderes als Absturz).

Dies ist die unmittelbarste Art von Kompromiss, den ich in meiner Domäne am häufigsten zwischen GC und keinem GC finde. Und ich interessiere mich eigentlich nicht sehr für GC in meiner Domäne, was nicht sehr geschäftskritisch ist, da die größten Probleme, die ich jemals mit undichter Software hatte, die zufällige Verwendung von GC in einem ehemaligen Team waren, die die oben beschriebenen Lecks verursachte .

In meiner speziellen Domäne bevorzuge ich es in vielen Fällen, dass die Software abstürzt oder ausfällt, da dies zumindest viel einfacher zu erkennen ist, als herauszufinden, warum die Software auf mysteriöse Weise explosive Speichermengen verbraucht, nachdem sie eine halbe Stunde lang ausgeführt wurde Geräte- und Integrationstests bestehen ohne Beanstandung (nicht einmal von Valgrind, da der Speicher beim Herunterfahren von GC freigegeben wird). Dies ist jedoch kein Slam für GC von meiner Seite oder ein Versuch zu sagen, dass es nutzlos ist oder so etwas, aber es war in den Teams, mit denen ich gegen undichte Software zusammengearbeitet habe, keine Silberkugel, auch nicht in der Nähe im Gegenteil, ich hatte die gegenteilige Erfahrung mit dieser einen Codebasis, bei der GC die leckeste ist, die mir jemals begegnet ist. Um fair zu sein, wussten viele Mitglieder dieses Teams nicht einmal, was schwache Referenzen waren.

Shared Ownership und Psychologie

Das Problem, das ich bei der Speicherbereinigung finde, die es so anfällig für "Speicherlecks" machen kann (und ich werde darauf bestehen, es als solches zu bezeichnen, wie das "Speicherleck" sich aus Sicht des Benutzers genauso verhält), liegt in den Händen von Diejenigen, die es nicht mit Sorgfalt verwenden, beziehen sich meiner Erfahrung nach zu einem gewissen Grad auf "menschliche Tendenzen". Das Problem mit diesem Team und der undichtesten Codebasis, auf die ich jemals gestoßen bin, war, dass sie den Eindruck hatten, dass GC es ihnen ermöglichen würde, nicht mehr darüber nachzudenken, wem Ressourcen gehören.

In unserem Fall hatten wir so viele Objekte, die sich gegenseitig referenzierten. Modelle verweisen zusammen mit der Materialbibliothek und dem Shader-System auf Materialien. Materialien verweisen auf Texturen zusammen mit der Texturbibliothek und bestimmten Shadern. Kameras speichern Verweise auf alle Arten von Szenenelementen, die vom Rendern ausgeschlossen werden sollten. Die Liste schien auf unbestimmte Zeit weiterzugehen. Dies führte dazu, dass fast jede umfangreiche Ressource im System an mehr als 10 anderen Stellen im Anwendungsstatus gleichzeitig besessen und auf Lebenszeit erweitert wurde, und das war sehr, sehr anfällig für menschliches Versagen, das sich in einem Leck niederschlug (und nicht) Ich spreche von Gigabyte in Minuten mit schwerwiegenden Usability-Problemen. Konzeptionell mussten alle diese Ressourcen nicht im Eigentum geteilt werden, sie hatten konzeptionell alle einen Eigentümer.

Wenn wir aufhören, darüber nachzudenken, wem welcher Speicher gehört, und glücklicherweise nur lebenslange Verweise auf Objekte überall speichern, ohne darüber nachzudenken, stürzt die Software nicht aufgrund von baumelnden Zeigern ab, sondern mit ziemlicher Sicherheit unter solchen Bedingungen Nachlässige Denkweise, fangen Sie an, Speicher wie verrückt auf eine Weise zu verlieren, die sehr schwer aufzuspüren ist und Tests entgeht.

Wenn der baumelnde Zeiger in meiner Domäne einen praktischen Vorteil hat, ist dies, dass er sehr schlimme Störungen und Abstürze verursacht. Und das gibt den Entwicklern zumindest den Anreiz, wenn sie etwas Zuverlässiges versenden möchten, über Ressourcenmanagement nachzudenken und die richtigen Maßnahmen zu ergreifen, um alle zusätzlichen Verweise / Zeiger auf ein Objekt zu entfernen, das konzeptionell nicht mehr benötigt wird.

Verwaltung von Anwendungsressourcen

Richtiges Ressourcenmanagement ist der Name des Spiels, wenn es darum geht, Lecks in langlebigen Anwendungen zu vermeiden, bei denen ein dauerhafter Zustand gespeichert wird, in dem Leckagen schwerwiegende Probleme mit der Framerate und der Benutzerfreundlichkeit verursachen würden. Und die korrekte Verwaltung von Ressourcen ist hier mit oder ohne GC nicht weniger schwierig. Die Arbeit ist nicht weniger manuell, um die entsprechenden Verweise auf nicht mehr benötigte Objekte zu entfernen, unabhängig davon, ob es sich um Zeiger oder lebenslange Referenzen handelt.

Das ist die Herausforderung in meinem Bereich, nicht zu vergessen, deletewas wir tun new(es sei denn, wir sprechen von einer Amateurstunde mit schlechten Tests, Praktiken und Werkzeugen). Und es erfordert Nachdenken und Sorgfalt, ob wir GC verwenden oder nicht.

Multithreading

Das andere Problem, das ich bei GC sehr nützlich finde, wenn es in meiner Domäne sehr vorsichtig verwendet werden kann, ist die Vereinfachung der Ressourcenverwaltung in Multithreading-Kontexten. Wenn wir darauf achten, lebenslange Verlängerungsreferenzen auf Ressourcen nicht an mehr als einer Stelle im Anwendungsstatus zu speichern, kann die lebenslange Verlängerung von GC-Referenzen äußerst nützlich sein, damit Threads eine Ressource, auf die zugegriffen wird, vorübergehend erweitern können, um sie zu erweitern Die Lebensdauer beträgt nur kurze Zeit, damit der Thread die Verarbeitung abschließen kann.

Ich denke, dass eine sehr sorgfältige Verwendung von GC auf diese Weise zu einer sehr korrekten Software führen kann, die nicht undicht ist und gleichzeitig das Multithreading vereinfacht.

Es gibt Möglichkeiten, dies zu umgehen, obwohl keine GC vorhanden ist. In meinem Fall vereinheitlichen wir die Darstellung der Szenenentität der Software mit Threads, die vorübergehend dazu führen, dass die Szenenressourcen vor einer Bereinigungsphase für kurze Zeit auf eine eher allgemeine Weise erweitert werden. Dies mag ein bisschen nach GC riechen, aber der Unterschied besteht darin, dass es sich nicht um ein "gemeinsames Eigentum" handelt, sondern nur um ein einheitliches Szenenverarbeitungsdesign in Threads, das die Zerstörung dieser Ressourcen verzögert. Dennoch wäre es viel einfacher, sich hier nur auf GC zu verlassen, wenn es für solche Multithreading-Fälle sehr sorgfältig mit gewissenhaften Entwicklern verwendet werden könnte, die darauf achten, schwache Referenzen in den relevanten persistenten Bereichen zu verwenden.

C ++

Schließlich:

In C ++ muss ich delete aufrufen, um ein erstelltes Objekt am Ende seines Lebenszyklus zu entsorgen.

In Modern C ++ sollten Sie dies im Allgemeinen nicht manuell tun. Es geht nicht einmal so sehr darum, zu vergessen, es zu tun. Wenn Sie die Ausnahmebehandlung in das Bild einbeziehen, kann, selbst wenn Sie einen entsprechenden deleteAufruf an unten geschrieben haben new, etwas in die Mitte werfen und den deleteAufruf nie erreichen , wenn Sie sich nicht auf automatisierte Destruktoraufrufe verlassen, für die der Compiler dies einfügt Du.

Mit C ++ müssen Sie praktisch eine solche manuelle Ressourcenbereinigung vermeiden (dies schließt das Vermeiden manueller Aufrufe zum Entsperren eines Mutex außerhalb eines dtors ein, es sei denn, Sie arbeiten in einem eingebetteten Kontext mit deaktivierten Ausnahmen und speziellen Bibliotheken, die absichtlich so programmiert sind, dass sie nicht ausgelöst werden zB und nicht nur Speicherfreigabe). Die Ausnahmebehandlung erfordert dies ziemlich genau, daher sollte die gesamte Ressourcenbereinigung größtenteils über Destruktoren automatisiert werden.

Drachenenergie
quelle