it-swarm.com.de

Wie ist die Speicherbereinigung mit der Referenzzählung zu vergleichen?

Ich arbeite an einem Online-Kurs zur iOS-Entwicklung in der neuen Sprache von Apple, Swift. Der Ausbilder machte einen Punkt, der diese Frage in meinem Kopf aufwirft. Er sagte etwas zu der Wirkung von:

Sie müssen sich keine Gedanken über die Speicherverwaltung machen, da alles durch Referenzzählung für Sie erledigt wird. Dies bedeutet, dass Sie sich keine Gedanken darüber machen müssen, wie die Speicherbereinigung funktioniert.

Als ich das hörte, dachte ich mir: "Warum sollte jemand die Speicherbereinigung verwenden, wenn Sie einfach die Referenzzählung verwenden können?"

Wie vergleichen sich die beiden Ansätze?

46
Aaron Anodide

Um zu verstehen, wie sich die beiden Ansätze vergleichen, müssen wir zunächst untersuchen, wie sie funktionieren und welche Schwächen sie haben.

Die automatische Referenzzählung oder ARC ist eine Form der Speicherbereinigung, bei der Objekte freigegeben werden, sobald keine Verweise mehr auf sie vorhanden sind, d. H. Keine andere Variable bezieht sich insbesondere auf das Objekt. Jedes Objekt unter ARC enthält einen Referenzzähler, der als zusätzliches Feld im Speicher gespeichert ist und jedes Mal erhöht wird, wenn Sie eine Variable für dieses Objekt festlegen (dh wenn ein neuer Verweis auf das Objekt erstellt wird), und jedes Mal, wenn Sie einen Wert festlegen, dekrementiert wird Ein Verweis auf das Objekt auf nil/null oder ein Verweis außerhalb des Gültigkeitsbereichs (dh er wird gelöscht, wenn der Stapel abgewickelt wird). Sobald der Referenzzähler auf Null gesunken ist, kümmert sich das Objekt darum, sich selbst zu löschen, den Destruktor aufzurufen und freizugeben der zugewiesene Speicher. Dieser Ansatz weist eine erhebliche Schwäche auf, wie wir weiter unten sehen werden.

"Sie müssen sich keine Gedanken über die Speicherverwaltung machen, da alles durch Referenzzählung für Sie erledigt wird." Dies ist eigentlich ein Missverständnis, das Sie immer noch beachten müssen, um bestimmte Bedingungen, nämlich Zirkelreferenzen, zu vermeiden, damit ARC ordnungsgemäß funktioniert. Eine Zirkelreferenz ist, wenn ein Objekt A eine starke Referenz auf ein Objekt B enthält, das selbst eine starke Referenz auf dasselbe Objekt A enthält. In dieser Situation wird keines der Objekte freigegeben, da A seinen Referenzzähler freigeben soll muss auf Null dekrementiert werden, aber mindestens eine dieser Referenzen ist Objekt B, damit Objekt B freigegeben werden kann, muss sein Referenzzähler ebenfalls auf 0 dekrementiert werden, aber mindestens eine dieser Referenzen ist Objekt A, können Sie das Problem sehen ? ARC löst dieses Problem, indem es dem Programmierer ermöglicht, dem Compiler Hinweise zu geben, wie unterschiedliche Objektreferenzen behandelt werden sollen. Es gibt zwei Arten von Referenzen: starke Referenzen und schwache Referenzen. Starke Referenzen sind, wie oben erwähnt, eine Art von Referenz, die die Lebensdauer des referenzierten Objekts verlängert (erhöht seinen Referenzzähler), schwache Referenzen sind eine Art von Referenz, die die Lebensdauer eines Objekts nicht verlängert (das heißt, es tut dies) den Referenzzähler des Objekts nicht erhöhen), aber das würde bedeuten, dass das referenzierte Objekt freigegeben werden könnte und Sie eine ungültige Referenz erhalten würden, die auf den Junk-Speicher verweist. Um diese Situation zu vermeiden, wird die schwache Referenz auf einen sicheren Wert gesetzt (z. B. Null in Ziel-C), sobald das Objekt freigegeben wird. Daher hat das Objekt eine zusätzliche Verantwortung, alle schwachen Referenzen zu verfolgen und auf diese zu setzen ein sicherer Wert, sobald er sich selbst löscht. Schwache Verweise werden normalerweise in einer Kind-Eltern-Objektbeziehung verwendet. Das Elternteil enthält einen starken Verweis auf alle untergeordneten Objekte, während die untergeordneten Objekte einen schwachen Verweis auf das Elternteil haben. In den meisten Fällen ist dies der Grund, warum Sie sich nicht mehr darum kümmern Als übergeordnetes Objekt interessieren Sie sich höchstwahrscheinlich auch nicht mehr für die untergeordneten Objekte.

Das Verfolgen der Speicherbereinigung (dh das, was am häufigsten als einfache Speicherbereinigung bezeichnet wird) umfasst das Führen einer Liste aller Stammobjekte (dh der in globalen Variablen gespeicherten Objekte, der lokalen Variablen der Hauptprozedur usw.) und das Verfolgen, welche Objekte erreichbar sind (Markieren) jedes angetroffene Objekt) von diesen Stammobjekten. Nachdem der Garbage Collector alle Objekte durchlaufen hat, auf die von den Stammobjekten verwiesen wird, durchläuft der GC nun jedes zugewiesene Objekt. Wenn es als erreichbar markiert ist, bleibt es im Speicher. Wenn es nicht als erreichbar markiert ist, wird es freigegeben. Dies ist bekannt als Mark-and-Sweep-Algorithmus. Dies hat den Vorteil, dass das Zirkelreferenzproblem nicht darunter leidet: Wenn weder das gegenseitig referenzierte Objekt A noch das Objekt B von einem anderen Objekt referenziert werden, das von den Stammobjekten aus erreichbar ist, werden weder Objekt A noch Objekt B als erreichbar markiert und beide freigegeben . Die Verfolgung von Garbage Collectors wird in bestimmten Intervallen ausgeführt, wobei alle Threads angehalten werden. Dies kann zu einer inkonsistenten Leistung führen (sporadische Pausen). Der hier beschriebene Algorithmus ist eine sehr grundlegende Beschreibung. Moderne GCs sind in der Regel mit einem Objekterzeugungssystem, Dreifarbensätzen usw. viel weiter fortgeschritten und führen auch andere Aufgaben aus, z. B. die Defragmentierung des Speicherplatzes des Programms, indem die Objekte in einen zusammenhängenden Speicher verschoben werden Dies ist der Grund, warum GC-Sprachen wie C # und Java keine Zeiger zulassen. Eine wesentliche Schwäche bei der Verfolgung von Garbage Collectors besteht darin, dass Klassendestruktoren nicht mehr deterministisch sind, dh der Programmierer Ich kann nicht sagen, wann ein Objekt durch Müll gesammelt werden soll. In GC-Sprachen kann der Programmierer nicht einmal einen Klassendestruktor angeben. Daher können Klassen nicht mehr zum Einkapseln der Verwaltung von Ressourcen wie Dateihandles und Datenbankverbindungen verwendet werden usw. Es liegt in der Verantwortung des Programmierers, geöffnete Dateien und Datenbankverbindungen manuell zu schließen. Daher haben Sprachen wie Java ein finally-Schlüsselwort (im try, catch-Block), um sicherzustellen, dass die sauber up-Code wird immer ausgeführt, bevor der Stapel abgewickelt wird, während in C++ (kein GC) solche Ressourcen von einem Wrapper-Objekt (auf dem Stapel zugeordnet) behandelt werden, das die Ressource im Konstruktor erfasst und im Destruktor freigibt, der immer als aufgerufen wird Das Objekt wird vom Stapel entfernt.

Was die Leistung betrifft, haben beide Leistungseinbußen. Die automatische Referenzzählung liefert eine konsistentere Leistung, keine Pausen, verlangsamt jedoch Ihre gesamte Anwendung, da jede Zuweisung eines Objekts zu einer Variablen, jede Freigabe eines Objekts usw. eine zugehörige Inkrementierung/Dekrementierung des Referenzzählers erfordert. und darauf achten, die schwachen Referenzen neu zuzuweisen und jeden Destruktor jedes Objekts aufzurufen, das freigegeben wird. GC hat nicht die Leistungsstrafe von ARC, wenn es um Objektreferenzen geht. Es kommt jedoch zu Pausen, während Müll gesammelt wird (was für Echtzeitverarbeitungssysteme unbrauchbar wird), und es ist ein großer Speicherplatz erforderlich, damit es effektiv funktioniert, sodass es nicht zur Ausführung gezwungen wird, wodurch die Ausführung zu oft angehalten wird.

Wie Sie sehen können, haben beide ihre eigenen Vor- und Nachteile. Es gibt keinen eindeutigen ARC, der besser oder GC ist besser. Beide sind Kompromisse.

PS : ARC wird auch problematisch, wenn Objekte über mehrere Threads hinweg gemeinsam genutzt werden, was eine atomare Inkrementierung/Dekrementierung des Referenzzählers erfordert, was selbst eine völlig neue Reihe von Komplexitäten und Problemen darstellt. Dies sollte Ihre Frage beantworten, warum jemand die Speicherbereinigung verwenden sollte.

71
ALXGTV

Der Ausbilder ist falsch. Sie wissen besser, wie Speicherbereinigung und Referenzzählung funktionieren.

Bei der Speicherbereinigung besteht das Problem darin, dass Sie möglicherweise noch einen Verweis auf ein Objekt in der Nähe hinterlassen haben. Es gab einen Fall, in dem ein frühes selbstfahrendes Fahrzeug abstürzte, weil Verweise auf Informationen über frühere Standorte in einem Array gespeichert wurden und nie zu Müll wurden. Nach 45 Minuten ging der Speicher aus und es stürzte ab. Ich denke nicht wörtlich, es hat aufgehört zu fahren, aber es könnte auch abgestürzt sein.

Bei der Referenzzählung besteht das Problem darin, dass Sie möglicherweise zyklische Referenzen A-> B-> A oder A-> B-> C -> ...-> Z-> A haben und keine Referenzzählung jemals auf Null geht. Deshalb haben Sie schwache Referenzen und müssen wissen, wann Sie sie verwenden müssen.

In beiden Fällen müssen Sie verstehen, wie die Dinge funktionieren, sonst werden Sie in Schwierigkeiten geraten. In Bezug auf die Leistung sagen sie, wenn Sie Java Entwickler fragen, dass die Speicherbereinigung schneller ist; wenn Sie fragen, ob Objective-C-Entwickler fragen, dass die Referenzzählung schneller ist. Studien beweisen, was sie beweisen wollen. Wenn es funktioniert Ein Unterschied ist, dass Sie die Anzahl der Zuordnungen reduzieren und nicht die Sprache wechseln sollten.

Sie müssen auch über schwache Referenzen Bescheid wissen, im Grunde eine Referenz auf ein Objekt, das das Objekt nicht am Leben hält. Und Sie müssen genau wissen, was passiert , wenn entschieden wurde, dass ein Objekt weggeworfen werden soll. in Java Ich denke, es gibt Möglichkeiten, wie ein Objekt wieder lebendig werden kann, in Objective-C/Swift Sobald der Referenzzähler Null ist, geht dieses Objekt wegzugehen, egal was Sie versuchen, daran festzuhalten. Nun, es sei denn, Sie fügen eine Zeile für (;;) hinzu; in der Dealloc/Deinit-Methode :-(

9
gnasher729

Manuelle Speicherverwaltung, Referenzzählung und Speicherbereinigung haben Vor- und Nachteile:

  • Manuelle Speicherverwaltung: Unschlagbar schnell, aber aufgrund von Fehlern bei der Speicherfreigabe fehleranfällig. Außerdem müssen Sie häufig mindestens die Referenzzählung zusätzlich zur manuellen Speicherverwaltung selbst implementieren, wenn Sie mehrere Objekte erhalten, für die alle ein einzelnes Objekt erforderlich ist, um am Leben zu bleiben.

  • Referenzzählung: Ein geringer Overhead (das Inkrementieren/Dekrementieren eines Zählers und eine Nullprüfung ist nicht so teuer) ermöglicht die einfache Verwaltung recht komplexer Datenstrukturen, bei denen jedes Objekt von mehreren anderen referenziert werden kann. Der Mangel besteht darin, dass die Referenzzählung erfordert, dass Referenzen nicht kreisförmig sind. Sobald Sie Referenzkreise erhalten, verlieren Sie Speicher.

    Schwache Referenzen können verwendet werden, um einige Referenzzyklen zu unterbrechen, sie verursachen jedoch einige zusätzliche Kosten:

    1. Schwache Referenzen erfordern einen zweiten Referenzzähler, um die schwache Referenz selbst zu verwalten. Wahrscheinlich ist die schwache Referenz ein weiteres Objekt, das unabhängig zugewiesen werden muss, was einen erheblichen Overhead beim Speicherverbrauch verursacht.

    2. Um ein Objekt bei Vorhandensein schwacher Referenzen zu zerstören, muss die schwache Referenz, die zum Objekt gehört, atomar zurückgesetzt und die Referenzanzahl verringert werden. Andernfalls erhalten Sie ein unberechenbares Verhalten der schwachen Referenzen. Ich bin nicht im Detail, aber ich glaube, dass dies schwer zu erreichen ist, wenn man keine Sperren hat.

    Dies kann alles getan werden, ist aber nicht so einfach wie das Zählen von Referenzen ohne schwache Referenzen.

  • Speicherbereinigung: Kann mit allen möglichen Abhängigkeitsdiagrammen umgehen, hat jedoch erhebliche Auswirkungen auf die Leistung. Schließlich muss der Garbage Collector irgendwie beweisen, dass ein Objekt nicht mehr erreichbar ist, bevor es es einsammeln kann. Moderne Müllsammler sind ziemlich gut darin, lange Verzögerungen bei ihrer Arbeit zu vermeiden, aber die Arbeit muss irgendwie erledigt werden. Dies ist besonders schlecht für Echtzeitanwendungen, die eine Antwort innerhalb eines bestimmten Zeitrahmens gewährleisten müssen.

Wie Sie sehen, haben alle drei Methoden Situationen, in denen sie am besten sind: Wenn Sie die manuelle Verwaltung problemlos durchführen können und Ihr Programm strenge Leistungsbeschränkungen aufweist, ist die manuelle Speicherverwaltung möglicherweise der richtige Weg. Und solange Sie die Referenzzählung über die Speicherbereinigung verwenden können, ist dies erheblich schneller und es treten keine falschen Verzögerungen auf. Trotzdem müssen Sie manchmal die Garbage Collection verwenden, da Sie keine zyklusfreien Referenzen garantieren können.

Sprachen, die die Garbage Collection für alles verwenden, haben gerade entschieden, dass die Benutzerfreundlichkeit für sie wichtiger ist als die Berücksichtigung leistungsabhängiger Anwendungen.