Oder mit anderen Worten, welche spezifischen Probleme hat die automatisierte Speicherbereinigung gelöst? Ich habe noch nie Low-Level-Programmierung durchgeführt, daher weiß ich nicht, wie kompliziert es sein kann, Ressourcen freizugeben.
Die Art von Fehlern, die GC anspricht, scheint (zumindest für einen externen Beobachter) die Art von Dingen zu sein, die ein Programmierer, der seine Sprache, Bibliotheken, Konzepte, Redewendungen usw. gut kennt, nicht tun würde. Aber ich könnte mich irren: Ist die manuelle Speicherverwaltung an sich kompliziert?
Antworten:
Komisch, wie sich die Definition von "Low-Level" im Laufe der Zeit ändert. Als ich das Programmieren zum ersten Mal lernte, wurde jede Sprache, die ein standardisiertes Heap-Modell bereitstellte, das ein einfaches Zuweisungs- / Freigabemuster ermöglicht, als hoch angesehen. Bei der Programmierung auf niedriger Ebene müssten Sie den Speicher selbst überwachen (nicht die Zuordnungen, sondern die Speicherorte selbst!) Oder Ihren eigenen Heap-Allokator schreiben, wenn Sie Lust dazu haben.
Davon abgesehen ist es überhaupt nicht beängstigend oder "kompliziert". Erinnerst du dich, als du ein Kind warst und deine Mutter hat dir gesagt, du sollst dein Spielzeug weglegen, wenn du damit fertig bist, dass sie nicht deine Magd ist und dein Zimmer nicht für dich aufräumen würde? Speicherverwaltung ist einfach dasselbe Prinzip, das auf Code angewendet wird. (GC ist wie eine Magd, die sich um Sie kümmert, aber sie ist sehr faul und leicht ahnungslos.) Das Prinzip ist einfach: Jede Variable in Ihrem Code hat einen und nur einen Eigentümer, und es liegt in der Verantwortung dieses Eigentümers Geben Sie den Speicher der Variablen frei, wenn diese nicht mehr benötigt wird. ( Das Prinzip des Alleineigentums) Dies erfordert einen Aufruf pro Zuordnung, und es gibt mehrere Schemata, die den Besitz und die Bereinigung auf die eine oder andere Weise automatisieren, sodass Sie diesen Aufruf nicht einmal in Ihren eigenen Code schreiben müssen.
Garbage Collection soll zwei Probleme lösen. Es macht immer einen sehr schlechten Job bei einem von ihnen, und abhängig von der Implementierung kann es bei dem anderen gut oder schlecht laufen. Die Probleme sind Speicherverluste (Festhalten am Speicher, nachdem Sie damit fertig sind) und baumelnde Referenzen (Freigeben von Speicher, bevor Sie damit fertig sind). Schauen wir uns beide Probleme an:
Baumelnde Referenzen: Diskutieren Sie diese zuerst, weil es die wirklich ernsthafte ist. Sie haben zwei Zeiger auf dasselbe Objekt. Sie befreien einen von ihnen und bemerken den anderen nicht. Dann versuchen Sie zu einem späteren Zeitpunkt, den zweiten zu lesen (oder zu schreiben oder freizugeben). Undefiniertes Verhalten folgt. Wenn Sie es nicht bemerken, können Sie Ihr Gedächtnis leicht beschädigen. Garbage Collection soll dieses Problem unmöglich machen, indem sichergestellt wird, dass niemals etwas freigegeben wird, bis alle Verweise darauf verschwunden sind. In einer vollständig verwalteten Sprache funktioniert dies fast, bis Sie sich mit externen, nicht verwalteten Speicherressourcen befassen müssen. Dann ist es gleich wieder Platz 1. Und in einer nicht verwalteten Sprache sind die Dinge noch schwieriger. (Stöbern Sie in Mozilla '
Glücklicherweise ist der Umgang mit diesem Problem im Grunde ein gelöstes Problem. Sie brauchen keinen Garbage Collector, Sie brauchen einen Debugging Memory Manager. Ich verwende zum Beispiel Delphi und kann mit einer einzigen externen Bibliothek und einer einfachen Compiler-Direktive den Allokator auf "Full Debug Mode" setzen. Dies fügt einen vernachlässigbaren (weniger als 5%) Leistungsaufwand hinzu, um einige Funktionen zu aktivieren, die den verwendeten Speicher verfolgen. Wenn ich ein Objekt freigebe, füllt es seinen Speicher mit
0x80
Bytes (im Debugger leicht erkennbar) und wenn ich jemals versuche, eine virtuelle Methode (einschließlich des Destruktors) für ein freigegebenes Objekt aufzurufen, bemerkt und unterbricht sie das Programm mit einer Fehlerbox mit drei Stapelspuren - als das Objekt erstellt wurde, Wann es befreit wurde und wo ich mich jetzt befinde - plus einige andere nützliche Informationen, löst dann eine Ausnahme aus. Dies ist offensichtlich nicht für Release-Builds geeignet, macht jedoch das Auffinden und Beheben von schwebenden Referenzproblemen trivial.Das zweite Problem sind Speicherverluste. Dies ist der Fall, wenn Sie den zugewiesenen Speicher weiter belassen, wenn Sie ihn nicht mehr benötigen. Dies kann in jeder Sprache mit oder ohne Garbage Collection geschehen und kann nur durch Schreiben Ihres Codes behoben werden. Garbage Collection hilft dabei, eine bestimmte Form des Speicherverlusts zu verringern , die auftritt, wenn Sie keine gültigen Verweise auf ein noch nicht freigegebenes Speicherelement haben. Dies bedeutet, dass der Speicher bis zum Ende des Programms zugewiesen bleibt. Leider ist die einzige Möglichkeit, dies auf automatisierte Weise zu erreichen, die Umwandlung jeder Zuordnung in ein Speicherverlust!
Ich werde mich wahrscheinlich von GC-Befürwortern verletzen lassen, wenn ich so etwas zu sagen versuche. Lassen Sie mich das erklären. Denken Sie daran, dass die Definition eines Speicherverlusts den zugewiesenen Speicher beibehält, wenn Sie ihn nicht mehr benötigen. Sie haben nicht nur keine Verweise auf etwas, sondern können auch Speicher verlieren, indem Sie einen unnötigen Verweis darauf haben, z. B. indem Sie ihn in einem Containerobjekt halten, wenn Sie ihn hätten freigeben sollen. Ich habe einige Speicherlecks gesehen, die dadurch verursacht wurden, und die sehr schwierig zu finden sind, ob Sie einen GC haben oder nicht, da sie einen perfekt gültigen Verweis auf den Speicher enthalten und es keine eindeutigen "Bugs" für das Debuggen von Tools gibt Fang. Soweit ich weiß, gibt es kein automatisiertes Tool, mit dem Sie diese Art von Speicherverlust abfangen können.
Ein Garbage Collector befasst sich also nur mit der Vielzahl von Speicherlecks, die keine Referenzen enthalten, da dies der einzige Typ ist, der automatisiert behandelt werden kann. Wenn es alle Ihre Verweise auf alles überwachen und jedes Objekt freigeben könnte, sobald keine Verweise darauf verweisen, wäre es perfekt, zumindest in Bezug auf das Problem der Nichtverweise. Dies auf automatisierte Weise zu tun , wird als Referenzzählung bezeichnet und kann in einigen begrenzten Situationen durchgeführt werden, hat jedoch seine eigenen Probleme. (Beispiel: Objekt A enthält einen Verweis auf Objekt B, der einen Verweis auf Objekt A enthält. In einem Referenzzählschema kann kein Objekt automatisch freigegeben werden, auch wenn keine externen Verweise auf A oder B vorhanden sind.) Also Müllsammler verwenden die AblaufverfolgungStattdessen: Beginnen Sie mit einer Reihe von Objekten, die als funktionierend bekannt sind, suchen Sie alle Objekte, auf die sie verweisen, suchen Sie alle Objekte, auf die sie verweisen, usw., bis Sie alles gefunden haben. Was beim Aufspüren nicht gefunden wird, ist Müll und kann weggeworfen werden. (Für eine erfolgreiche Ausführung ist natürlich eine verwaltete Sprache erforderlich, die bestimmte Einschränkungen für das Typsystem enthält, um sicherzustellen, dass der Tracing-Garbage-Collector immer den Unterschied zwischen einer Referenz und einem zufälligen Teil des Speichers erkennen kann, der zufällig wie ein Zeiger aussieht.)
Es gibt zwei Probleme bei der Ablaufverfolgung. Erstens ist es langsam und währenddessen muss das Programm mehr oder weniger pausiert werden, um Rennbedingungen zu vermeiden. Dies kann zu spürbaren Ausführungsproblemen führen, wenn das Programm mit einem Benutzer interagieren soll, oder zu einer schlechten Leistung in einer Server-App. Dies kann durch verschiedene Techniken gemildert werden, z. B. das Aufteilen des zugewiesenen Speichers in "Generationen" nach dem Prinzip, dass es wahrscheinlich eine Weile dauern wird, bis eine Zuordnung beim ersten Versuch nicht erfasst wird. Sowohl das .NET Framework als auch die JVM verwenden generationsübergreifende Garbage Collectors.
Leider führt dies zu dem zweiten Problem: Speicher wird nicht freigegeben, wenn Sie damit fertig sind. Wenn die Ablaufverfolgung nicht unmittelbar nach dem Beenden eines Objekts ausgeführt wird, bleibt sie bis zur nächsten Ablaufverfolgung bestehen oder sogar noch länger, wenn die erste Generation überschritten wird. Tatsächlich erklärt eine der besten Erklärungen des .NET-Garbage Collectors , dass der GC die Erfassung so lange wie möglich verschieben muss, um den Prozess so schnell wie möglich zu gestalten. Das Problem der Speicherlecks wird also ziemlich seltsam gelöst, indem so lange wie möglich so viel Speicher wie möglich verloren geht! Dies ist, was ich meine, wenn ich sage, dass ein GC jede Zuordnung in einen Speicherverlust verwandelt. Tatsächlich gibt es keine Garantie dafür, dass ein bestimmtes Objekt jemals abgeholt wird.
Warum ist dies ein Problem, wenn der Speicher bei Bedarf immer noch zurückgefordert wird? Aus mehreren Gründen. Stellen Sie sich zunächst vor, Sie ordnen ein großes Objekt zu (z. B. eine Bitmap), das viel Speicher benötigt. Und kurz nachdem Sie damit fertig sind, benötigen Sie ein weiteres großes Objekt, das die gleiche (oder nahezu die gleiche) Speichermenge benötigt. Wurde das erste Objekt freigegeben, kann das zweite sein Gedächtnis wiederverwenden. Auf einem System mit Speicherbereinigung warten Sie möglicherweise immer noch auf die Ausführung der nächsten Ablaufverfolgung, sodass unnötigerweise Speicher für ein zweites großes Objekt verschwendet wird. Es ist im Grunde eine Rennbedingung.
Zweitens kann das unnötige Halten des Speichers, insbesondere in großen Mengen, in einem modernen Multitasking-System Probleme verursachen. Wenn Sie zu viel physischen Speicher belegen, muss Ihr Programm oder andere Programme möglicherweise paginieren (einen Teil ihres Speichers auf Disc auslagern), was die Geschwindigkeit erheblich senkt. Bei bestimmten Systemen, wie z. B. Servern, kann Paging nicht nur das System verlangsamen, sondern auch das Ganze zum Absturz bringen, wenn es unter Last steht.
Wie das Problem der baumelnden Referenzen kann das Problem der fehlenden Referenzen mit einem Debugging-Speichermanager gelöst werden. Ich erwähne noch einmal den vollständigen Debug-Modus von Delphis FastMM-Speichermanager, da er derjenige ist, mit dem ich am vertrautesten bin. (Ich bin sicher, dass ähnliche Systeme für andere Sprachen existieren.)
Wenn ein Programm, das unter FastMM ausgeführt wird, beendet wird, können Sie optional angeben, dass alle Zuordnungen vorhanden sind, die nie freigegeben wurden. Der vollständige Debug-Modus geht noch einen Schritt weiter: Er kann eine Datei auf einem Datenträger speichern, der nicht nur die Art der Zuweisung, sondern auch eine Stapelverfolgung von der Zuweisung an und andere Debug-Informationen für jede durchgesickerte Zuweisung enthält. Dies macht das Auffinden von Speicherlecks ohne Verweise trivial.
Wenn Sie sich das wirklich ansehen, kann die Garbage Collection möglicherweise nicht gut genug sein, um herabhängende Referenzen zu verhindern, und im Allgemeinen ist die Behandlung von Speicherlecks eine schlechte Sache. Tatsächlich ist die eine Tugend nicht die Speicherbereinigung selbst, sondern ein Nebeneffekt: Sie bietet eine automatisierte Möglichkeit zur Durchführung der Haufenverdichtung. Dies kann ein arkanes Problem (Speichererschöpfung durch Heap-Fragmentierung) verhindern, das Programme, die über einen langen Zeitraum ausgeführt werden und einen hohen Grad an Speicherabwanderung aufweisen, zum Erliegen bringen kann, und eine Heap-Komprimierung ist ohne Garbage Collection so gut wie unmöglich. Heutzutage verwendet ein guter Speicherzuordner jedoch Buckets, um die Fragmentierung zu minimieren, was bedeutet, dass die Fragmentierung nur unter extremen Umständen zu einem echten Problem wird. Bei einem Programm, bei dem die Heap-Fragmentierung wahrscheinlich ein Problem darstellt, Es wird empfohlen, einen Müllsammler zu verwenden. Aber in jedem anderen Fall ist die Verwendung der Speicherbereinigung eine vorzeitige Optimierung, und es gibt bessere Lösungen für die Probleme, die sie "löst".
quelle
Betrachtet man eine Speicherverwaltungstechnik ohne Speicherbereinigung aus einer ähnlichen Zeit wie die Speicherbereinigung, die in gängigen Systemen wie C ++ RAII verwendet wird. Angesichts dieses Ansatzes sind die Kosten für die Nichtverwendung der automatischen Speicherbereinigung minimal, und GC führt zu zahlreichen eigenen Problemen. Daher würde ich vorschlagen, dass "Nicht viel" die Antwort auf Ihr Problem ist.
Denken Sie daran, wenn Menschen an Nicht-GC denken, denken sie an
malloc
undfree
. Aber dies ist ein riesiger logischer Irrtum - Sie würden das Management von Nicht-GC-Ressourcen der frühen 1970er Jahre mit den Müllsammlern der späten 90er Jahre vergleichen. Dies ist offensichtlich ein ziemlich unfairer Vergleich - die Garbage Collectors, die zu der Zeit verwendet wurdenmalloc
undfree
entworfen wurden, waren viel zu langsam, um ein sinnvolles Programm auszuführen, wenn ich mich richtig erinnere.unique_ptr
Viel aussagekräftiger ist beispielsweise der Vergleich von etwas aus einem vage äquivalenten Zeitraum .Müllsammler können mit Referenzzyklen einfacher umgehen, obwohl dies nur sehr selten vorkommt. Darüber hinaus können GCs nur Code "ausgeben", da sich der GC um die gesamte Speicherverwaltung kümmert, was bedeutet, dass sie zu schnelleren Entwicklungszyklen führen können.
Auf der anderen Seite neigen sie dazu, massive Probleme beim Umgang mit Speicher zu bekommen, der von irgendwoher kam, außer von ihrem eigenen GC-Pool. Darüber hinaus verlieren sie einen Großteil ihres Nutzens, wenn es sich um Nebenläufigkeiten handelt, da Sie ohnehin den Besitz von Objekten berücksichtigen müssen.
Bearbeiten: Viele der Dinge, die Sie erwähnen, haben nichts mit GC zu tun. Sie verwechseln Speicherverwaltung und Objektorientierung. Sehen Sie, hier ist die Sache: Wenn Sie in einem vollständig nicht verwalteten System wie C ++ programmieren, können Sie so viele Grenzen prüfen, wie Sie möchten, und die Standard-Containerklassen bieten dies an. Es gibt zum Beispiel nichts, wenn Grenzen überprüft oder stark getippt werden.
Die von Ihnen genannten Probleme werden durch Objektorientierung und nicht durch GC gelöst. Der Ursprung des Array-Speichers und das Sicherstellen, dass Sie nicht außerhalb desselben schreiben, sind orthogonale Konzepte.
Bearbeiten: Es ist erwähnenswert, dass fortgeschrittenere Techniken die Notwendigkeit einer dynamischen Speicherzuweisung überhaupt vermeiden können. Betrachten Sie beispielsweise die Verwendung von this , das Y-Combination in C ++ ohne dynamische Zuweisung implementiert.
quelle
Die "Freiheit, sich keine Sorgen um die Freigabe von Ressourcen machen zu müssen", die müllsammelnde Sprachen angeblich bieten, ist in erheblichem Maße eine Illusion. Fügen Sie immer wieder Dinge zu einer Karte hinzu, ohne sie zu entfernen, und Sie werden bald verstehen, wovon ich spreche.
In der Tat treten in Programmen, die in GC-Sprachen geschrieben wurden, häufig Speicherverluste auf, da diese Sprachen die Programmierer fauler machen und ihnen ein falsches Gefühl der Sicherheit vermitteln, dass die Sprache jedes Objekt, das sie bearbeiten, immer irgendwie (magisch) pflegt Ich möchte nicht mehr darüber nachdenken müssen.
Die Speicherbereinigung ist einfach eine notwendige Einrichtung für Sprachen, die ein anderes, edleres Ziel haben: alles als Zeiger auf ein Objekt zu behandeln und gleichzeitig vor dem Programmierer die Tatsache zu verbergen, dass es sich um einen Zeiger handelt, so dass der Programmierer kein Commit durchführen kann Selbstmord durch Zeigerarithmetik und dergleichen. Alles, was ein Objekt ist, bedeutet, dass GCed-Sprachen Objekte viel häufiger zuordnen müssen als nicht-GCed-Sprachen. Wenn sie dem Programmierer die Last der Freigabe dieser Objekte auferlegen, wären sie immens unattraktiv.
Garbage Collection ist auch nützlich, um dem Programmierer die Möglichkeit zu geben, engen Code zu schreiben, Objekte in Ausdrücken auf eine funktionale Programmierweise zu manipulieren, ohne die Ausdrücke in separate Anweisungen aufteilen zu müssen, um die Zuordnung für alle aufzuheben einzelnes Objekt, das an dem Ausdruck beteiligt ist.
Ansonsten sei angemerkt, dass ich zu Beginn meiner Antwort geschrieben habe: "Es ist in erheblichem Maße eine Illusion." Ich habe nicht geschrieben, dass es eine Illusion ist. Ich habe nicht einmal geschrieben, dass es hauptsächlich eine Illusion ist. Garbage Collection ist nützlich, um dem Programmierer die grundlegende Aufgabe zu nehmen, sich um die Freigabe seiner Objekte zu kümmern. In diesem Sinne handelt es sich also um ein Produktivitätsmerkmal.
quelle
Der Garbage Collector behebt keine "Bugs". Es ist ein notwendiger Bestandteil einiger Hochsprachen-Semantik. Mit einem GC können höhere Abstraktionsebenen definiert werden, z. B. lexikalische Closures und dergleichen, während diese Abstraktionen bei einer manuellen Speicherverwaltung undicht sind und unnötigerweise an die niedrigeren Ebenen der Ressourcenverwaltung gebunden sind.
Ein in den Kommentaren erwähntes "Prinzip der einmaligen Inhaberschaft" ist ein gutes Beispiel für eine solche undichte Abstraktion. Ein Entwickler sollte sich überhaupt keine Gedanken über die Anzahl der Verknüpfungen zu einer bestimmten elementaren Datenstrukturinstanz machen, da sonst ein Codeteil ohne eine Vielzahl zusätzlicher (im Code selbst nicht direkt sichtbarer) Einschränkungen und Anforderungen nicht generisch und transparent wäre . Ein solcher Code kann nicht zu einem Code auf höherer Ebene zusammengesetzt werden, was einen unerträglichen Verstoß gegen das Prinzip der Aufteilung der Verantwortungsebenen darstellt (ein wesentlicher Baustein des Software-Engineerings, der von den meisten Entwicklern auf niedriger Ebene leider überhaupt nicht beachtet wird).
quelle
In der Tat ist die Verwaltung des eigenen Speichers nur eine weitere potenzielle Fehlerquelle.
Wenn Sie einen Aufruf von vergessen
free
(oder was auch immer die Entsprechung in der von Ihnen verwendeten Sprache ist), kann Ihr Programm alle Tests bestehen, aber Speicher lecken. Und in einem moderat komplexen Programm ist es ziemlich einfach, einen Anruf zu übersehenfree
.quelle
free
ist nicht das Schlimmste. Frühfree
ist viel verheerender.free
!malloc
undfree
die nicht zum GC gehörten, waren viel zu langsam, um für irgendetwas von Nutzen zu sein. Sie müssen es mit einem modernen Nicht-GC-Ansatz wie RAII vergleichen.Manuelle Ressourcen sind nicht nur mühsam, sondern auch schwer zu debuggen. Mit anderen Worten, es ist nicht nur mühsam, es richtig zu machen, sondern auch, wenn Sie es falsch verstehen, ist es nicht offensichtlich, wo das Problem liegt. Dies liegt daran, dass sich die Auswirkungen des Fehlers im Gegensatz zu beispielsweise einer Division durch Null nicht auf die Fehlerquelle auswirken. Das Verbinden der Punkte erfordert Zeit, Aufmerksamkeit und Erfahrung.
quelle
Ich denke, dass die Garbage Collection eine Menge Anerkennung für Sprachverbesserungen erhält, die nichts mit GC zu tun haben, außer Teil einer großen Fortschrittswelle zu sein.
Der einzige Vorteil von GC, den ich kenne, ist, dass Sie ein Objekt in Ihrem Programm freigeben können und wissen, dass es verschwindet, wenn alle damit fertig sind. Sie können es an die Methode einer anderen Klasse übergeben, ohne sich darum zu kümmern. Es ist Ihnen egal, an welche anderen Methoden es übergeben wird oder welche anderen Klassen darauf verweisen. (Speicherverluste liegen in der Verantwortung der Klasse, die auf ein Objekt verweist, nicht der Klasse, die es erstellt hat.)
Ohne GC müssen Sie den gesamten Lebenszyklus des zugewiesenen Speichers verfolgen. Jedes Mal, wenn Sie eine Adresse von der Subroutine, die sie erstellt hat, nach oben oder unten übergeben, haben Sie einen unkontrollierten Verweis auf diesen Speicher. In der schlechten alten Zeit war es mir trotz nur eines Threads aufgrund der Rekursion und des Betriebssystems (Windows NT) nicht möglich, den Zugriff auf den zugewiesenen Speicher zu kontrollieren. Ich musste die freie Methode in meinem eigenen Zuordnungssystem manipulieren, um Speicherblöcke eine Zeit lang zu behalten, bis alle Referenzen gelöscht waren. Die Haltezeit war reine Vermutung, aber es hat funktioniert.
Das ist der einzige GC-Vorteil, den ich kenne, aber ich könnte nicht ohne ihn leben. Ich denke nicht, dass irgendeine Art von OOP ohne sie fliegen wird.
quelle
Physische Lecks
Wenn ich von der C-Seite komme, die die Speicherverwaltung so manuell und ausgeprägt wie möglich macht, damit wir Extreme vergleichen (C ++ automatisiert die Speicherverwaltung meist ohne GC), würde ich sagen, "nicht wirklich" im Sinne eines Vergleichs mit GC, wenn dies der Fall ist kommt zu undichtigkeiten . Ein Anfänger und manchmal sogar ein Profi kann vergessen,
free
für eine gegebene zu schreibenmalloc
. Es passiert definitiv.Es gibt jedoch Tools wie die
valgrind
Lecksuche, die bei der Ausführung des Codes sofort erkennen, wann / wo solche Fehler bis auf die genaue Codezeile auftreten. Wenn dies in das CI integriert ist, ist es fast unmöglich, solche Fehler zusammenzuführen und einfach zu korrigieren. Es ist also in keinem Team / Prozess eine große Sache mit vernünftigen Standards.Zugegeben, es kann einige exotische Fälle von Ausführung geben, die unter dem Radar des Testens
free
auftauchen, wenn sie nicht aufgerufen werden, möglicherweise wenn ein obskurer externer Eingabefehler wie eine beschädigte Datei auftritt. In diesem Fall kann das System 32 Bytes oder etwas verlieren. Ich denke, das kann definitiv auch unter recht guten Teststandards und Leckerkennungswerkzeugen passieren, aber es wäre auch nicht ganz so kritisch, ein bisschen Speicher für etwas zu verlieren, das so gut wie nie passiert. Es wird ein weitaus größeres Problem geben, bei dem wir auch in den unten aufgeführten allgemeinen Ausführungspfaden massive Ressourcen verlieren können, was GC nicht verhindern kann.Es ist auch schwierig, ohne etwas zu tun, das einer Pseudoform von GC ähnelt (z. B. Referenzzählung), wenn die Lebensdauer eines Objekts für eine Form von verzögerter / asynchroner Verarbeitung verlängert werden muss, z. B. durch einen anderen Thread.
Baumelnde Zeiger
Das eigentliche Problem mit mehr manuellen Formen der Speicherverwaltung ist für mich nicht zu übersehen. Wie viele native Anwendungen, die in C oder C ++ geschrieben wurden, sind wirklich undicht? Ist der Linux-Kernel undicht? MySQL? CryEngine 3? Digitale Audio-Workstations und Synthesizer? Leckt Java VM (im nativen Code implementiert)? Photoshop?
Ich denke, wenn wir uns umschauen, sind die am wenigsten geeigneten Anwendungen diejenigen, die mit GC-Schemata geschrieben wurden. Bevor dies jedoch als Slam-on-Garbage-Collection betrachtet wird, tritt bei systemeigenem Code ein erhebliches Problem auf, das überhaupt nicht mit Speicherverlusten zusammenhängt.
Das Thema war für mich immer Sicherheit. Selbst wenn wir uns
free
durch einen Zeiger erinnern, werden andere Zeiger auf die Ressource zu baumelnden (ungültigen) Zeigern.Wenn wir versuchen, auf die Spitzen dieser baumelnden Zeiger zuzugreifen, geraten wir letztendlich in ein undefiniertes Verhalten, obwohl fast immer eine Segfault- / Zugriffsverletzung zu einem harten, sofortigen Absturz führt.
Alle diese nativen Anwendungen, die ich oben aufgeführt habe, weisen möglicherweise ein oder zwei unklare Randbedingungen auf, die in erster Linie aufgrund dieses Problems zu einem Absturz führen können, und es gibt definitiv einen angemessenen Anteil von fehlerhaften Anwendungen, die in nativem Code geschrieben sind, der sehr absturzintensiv und häufig ist zum großen Teil aufgrund dieses Problems.
... und das liegt daran, dass das Ressourcenmanagement schwierig ist, unabhängig davon, ob Sie GC verwenden oder nicht. Der praktische Unterschied besteht häufig darin, dass der Computer aufgrund eines Fehlers, der zu einem Missmanagement der Ressourcen führt, leckt (GC) oder abstürzt (ohne GC).
Ressourcenverwaltung: Garbage Collection
Ein komplexes Ressourcenmanagement ist in jedem Fall ein schwieriger manueller Prozess. GC kann hier nichts automatisieren.
Nehmen wir ein Beispiel, wo wir dieses Objekt haben, "Joe". Auf Joe wird von einer Reihe von Organisationen verwiesen, denen er angehört. Jeden Monat oder so ziehen sie einen Mitgliedsbeitrag von seiner Kreditkarte ab.
Wir haben auch einen Hinweis auf Joe, um sein Leben zu kontrollieren. Nehmen wir an, als Programmierer brauchen wir Joe nicht mehr. Er fängt an, uns zu belästigen, und wir brauchen diese Organisationen nicht mehr, denen er angehört, und verschwenden ihre Zeit damit, sich um ihn zu kümmern. Also versuchen wir, ihn vom Erdboden zu wischen, indem wir seinen Lebenslinienbezug entfernen.
... aber warte, wir verwenden Garbage Collection. Jeder starke Hinweis auf Joe wird ihn in der Nähe halten. Deshalb entfernen wir auch Verweise auf ihn aus den Organisationen, denen er angehört (ihn abbestellen).
... außer Hoppla, wir haben vergessen, sein Zeitschriftenabonnement zu kündigen! Jetzt bleibt Joe im Gedächtnis, belästigt uns und verbraucht Ressourcen, und das Zeitschriftenunternehmen arbeitet jeden Monat an der Bearbeitung von Joes Mitgliedschaft.
Dies ist der Hauptfehler, der dazu führen kann, dass viele komplexe Programme, die mit Garbage Collection-Schemata geschrieben wurden, auslaufen und mehr und mehr Speicher verbrauchen, je länger sie ausgeführt werden und möglicherweise mehr und mehr verarbeitet werden (das Abonnement für wiederkehrende Magazine). Sie vergaßen, einen oder mehrere dieser Verweise zu entfernen, was es dem Müllsammler unmöglich machte, seine Magie zu entfalten, bis das gesamte Programm heruntergefahren wurde.
Das Programm stürzt jedoch nicht ab. Es ist absolut sicher. Es wird nur die Erinnerung auffrischen und Joe wird immer noch verweilen. Für viele Anwendungen ist diese Art von undichtem Verhalten, bei dem immer mehr Speicher / Verarbeitung zum Problem wird, einem harten Absturz weitaus vorzuziehen, insbesondere angesichts der Größe des Arbeitsspeichers und der Rechenleistung, über die unsere Maschinen heutzutage verfügen.
Ressourcenverwaltung: Manuell
Betrachten wir nun die Alternative, bei der wir Zeiger auf Joe und manuelle Speicherverwaltung verwenden, wie folgt:
Diese blauen Links verwalten nicht Joes Leben. Wenn wir ihn vom Erdboden entfernen wollen, fordern wir ihn manuell auf, wie folgt zu zerstören:
Nun, das würde uns normalerweise überall mit baumelnden Zeigern zurücklassen, also lasst uns die Zeiger auf Joe entfernen.
... hoppla, wir haben genau den gleichen Fehler gemacht und vergessen, Joes Abonnement zu kündigen!
Außer jetzt haben wir einen baumelnden Zeiger. Wenn das Zeitschriftenabonnement versucht, Joes monatliche Gebühr zu verarbeiten, explodiert die ganze Welt - normalerweise bekommen wir den schweren Absturz sofort.
Derselbe grundlegende Fehler beim Missmanagement von Ressourcen, bei dem der Entwickler vergessen hat, alle Zeiger / Verweise auf eine Ressource manuell zu entfernen, kann in nativen Anwendungen zu zahlreichen Abstürzen führen. Sie belasten den Speicher nicht, je länger sie normalerweise ausgeführt werden, da sie in diesem Fall häufig regelrecht abstürzen.
Echte Welt
Das obige Beispiel verwendet nun ein lächerlich einfaches Diagramm. Für eine reale Anwendung sind möglicherweise Tausende von Bildern erforderlich, die zusammengefügt werden, um ein vollständiges Diagramm abzudecken. In einem Szenendiagramm sind Hunderte verschiedener Ressourcentypen gespeichert, einige davon mit GPU-Ressourcen verknüpft, andere mit Beschleunigern verknüpft und Beobachter auf Hunderte von Plugins verteilt Beobachten Sie eine Reihe von Entitätstypen in der Szene auf Änderungen, Beobachter, Beobachter, Audios, die mit Animationen synchronisiert sind usw. Es scheint also, als wäre es einfach, den oben beschriebenen Fehler zu vermeiden, aber in der realen Welt ist es im Allgemeinen nicht annähernd so einfach Produktionscodebasis für eine komplexe Anwendung, die Millionen von Codezeilen umfasst.
Die Wahrscheinlichkeit, dass jemand eines Tages die Ressourcen irgendwo in dieser Codebasis falsch verwaltet, ist recht hoch, und diese Wahrscheinlichkeit ist mit oder ohne GC gleich. Der Hauptunterschied besteht darin, was als Ergebnis dieses Fehlers passieren wird. Dies wirkt sich auch potenziell darauf aus, wie schnell dieser Fehler entdeckt und behoben wird.
Crash vs. Leak
Welches ist nun schlimmer? Ein sofortiger Absturz oder ein stilles Gedächtnisleck, in dem Joe auf mysteriöse Weise herumlungert?
Die meisten antworten vielleicht auf Letzteres, aber nehmen wir an, diese Software ist so konzipiert, dass sie stundenlang, möglicherweise tagelang ausgeführt wird, und jeder dieser von uns hinzugefügten Joes und Janes erhöht die Speichernutzung der Software um ein Gigabyte. Es ist keine geschäftskritische Software (Abstürze töten Benutzer nicht wirklich), sondern eine leistungskritische.
In diesem Fall ist ein schwerer Absturz, der beim Debuggen sofort auftritt und auf den von Ihnen begangenen Fehler hinweist, möglicherweise einer undichten Software vorzuziehen, die möglicherweise sogar unter dem Radar Ihres Testverfahrens läuft.
Auf der anderen Seite, wenn es sich um eine unternehmenskritische Software handelt, bei der die Leistung nicht das Ziel ist und die auf keinen Fall abstürzt, ist eine Leckage möglicherweise sogar vorzuziehen.
Schwache Referenzen
Es gibt eine Art Hybrid dieser Ideen in GC-Schemata, die als schwache Referenzen bekannt sind. Mit schwachen Referenzen können wir alle diese Organisationen so einrichten, dass sie Joe schwach referenzieren, ihn jedoch nicht daran hindern, entfernt zu werden, wenn die starke Referenz (Joes Besitzer / Lebensader) wegfällt. Trotzdem haben wir den Vorteil, durch diese schwachen Referenzen feststellen zu können, wann Joe nicht mehr in der Nähe ist, und so eine Art leicht reproduzierbarer Fehler erhalten zu können.
Leider werden schwache Referenzen bei weitem nicht so oft verwendet, wie sie wahrscheinlich verwendet werden sollten, so dass viele komplexe GC-Anwendungen möglicherweise für Lecks anfällig sind, selbst wenn sie möglicherweise weitaus weniger abstürzen als eine komplexe C-Anwendung, z
In jedem Fall hängt es davon ab, wie wichtig es ist, dass Ihre Software Leckagen vermeidet, und ob es sich um ein komplexes Ressourcenmanagement dieser Art handelt oder nicht.
In meinem Fall arbeite ich in einem leistungskritischen Bereich, in dem Ressourcen Hunderte von Megabyte bis Gigabyte umfassen, und wenn Benutzer aufgrund eines Fehlers wie des oben genannten das Entladen anfordern, wird es einem Absturz weniger vorgezogen, diesen Speicher nicht freizugeben . Abstürze sind leicht zu erkennen und zu reproduzieren, was sie häufig zu den beliebtesten Fehlern des Programmierers macht, auch wenn sie die am wenigsten bevorzugten Fehler des Benutzers sind. Viele dieser Abstürze treten mit einem vernünftigen Testverfahren auf, bevor sie den Benutzer überhaupt erreichen.
Das sind jedenfalls die Unterschiede zwischen GC und manueller Speicherverwaltung. Zur Beantwortung Ihrer unmittelbaren Frage würde ich sagen, dass die manuelle Speicherverwaltung schwierig ist, aber nur sehr wenig mit Lecks zu tun hat. Sowohl die GC- als auch die manuelle Speicherverwaltung sind nach wie vor sehr schwierig, wenn die Ressourcenverwaltung nicht trivial ist. Der GC hat hier wohl ein kniffligeres Verhalten, bei dem das Programm anscheinend einwandfrei funktioniert, aber immer mehr Ressourcen verbraucht. Die manuelle Form ist weniger knifflig, wird aber mit Fehlern wie dem oben gezeigten sehr schnell zum Absturz gebracht.
quelle
Hier ist eine Liste der Probleme, mit denen C ++ - Programmierer beim Umgang mit Speicher konfrontiert sind:
Wie Sie sehen, löst der Heap-Speicher sehr viele bestehende Probleme, verursacht jedoch zusätzliche Komplexität. GC wurde entwickelt, um einen Teil dieser Komplexität zu bewältigen. (Entschuldigung, wenn einige Problemnamen nicht die richtigen Namen für diese Probleme sind - manchmal ist es schwierig, den richtigen Namen zu finden)
quelle