it-swarm.com.de

Warum werden Java Objekte nicht sofort gelöscht, nachdem sie nicht mehr referenziert wurden?

In Java kann ein Objekt, sobald es keine Referenzen mehr hat, gelöscht werden. Die JVM entscheidet jedoch, wann das Objekt tatsächlich gelöscht wird. Um die Objective-C-Terminologie zu verwenden, sind alle Java-Referenzen von Natur aus "stark". Wenn in Objective-C ein Objekt keine starken Referenzen mehr enthält, wird das Objekt sofort gelöscht. Warum nicht? Ist das in Java nicht der Fall?

79
moonman239

Zuallererst hat Java hat schwache Referenzen und eine andere Best-Effort-Kategorie, die als weiche Referenzen bezeichnet wird. Schwache oder starke Referenzen sind ein völlig anderes Problem als das Zählen von Referenzen im Vergleich zur Speicherbereinigung.

Zweitens gibt es Muster in der Speichernutzung, die die Speicherbereinigung zeitlich effizienter machen können, indem Platz gespart wird. Beispielsweise werden neuere Objekte viel häufiger gelöscht als ältere Objekte. Wenn Sie also zwischen den Durchläufen etwas warten, können Sie den größten Teil der neuen Speichergeneration löschen und gleichzeitig die wenigen Überlebenden in einen längerfristigen Speicher verschieben. Dieser Langzeitspeicher kann viel seltener gescannt werden. Das sofortige Löschen durch manuelle Speicherverwaltung oder Referenzzählung ist viel anfälliger für Fragmentierung.

Es ist ein bisschen wie der Unterschied zwischen einem Einkauf pro Gehaltsscheck und einem täglichen Einkauf, um gerade genug Essen für einen Tag zu bekommen. Ihre eine große Reise dauert viel länger als eine einzelne kleine Reise, aber insgesamt sparen Sie Zeit und wahrscheinlich Geld.

79
Karl Bielefeldt

Weil es nicht einfach ist, etwas richtig zu wissen, auf das nicht mehr verwiesen wird. Nicht einmal annähernd einfach.

Was ist, wenn zwei Objekte aufeinander verweisen? Bleiben sie für immer? Wenn Sie diese Denkweise auf die Auflösung beliebiger Datenstrukturen ausweiten, werden Sie bald feststellen, warum die JVM oder andere Garbage Collectors gezwungen sind, weitaus ausgefeiltere Methoden anzuwenden, um festzustellen, was noch benötigt wird und was noch möglich ist.

86
whatsisname

AFAIK, die JVM-Spezifikation (in Englisch geschrieben) erwähnt nicht wann genau ein Objekt (oder ein Wert) sollte gelöscht werden und überlässt dies der Implementierung (ebenfalls) für R5RS ). Es erfordert oder schlägt irgendwie einen Garbage Collector vor, überlässt die Details jedoch der Implementierung. Und ebenfalls für die Spezifikation Java.

Denken Sie daran, dass Programmiersprachen Spezifikationen (von Syntax , Semantik usw.) sind, keine Software Implementierungen. Eine Sprache wie Java (oder ihre JVM) hat viele Implementierungen. Die Spezifikation ist veröffentlicht , herunterladbar (damit Sie sie studieren können) und in englischer Sprache verfasst. §2.5.3 Heap der JVM-Spezifikation erwähnt einen Garbage Collector:

Der Heap-Speicher für Objekte wird von einem automatischen Speicherverwaltungssystem (dem so genannten Garbage Collector) zurückgefordert. Objekte werden niemals explizit freigegeben. Die virtuelle Maschine Java nimmt keinen bestimmten Typ eines automatischen Speicherverwaltungssystems an .

(Hervorhebung ist meine; BTW-Finalisierung wird in §12.6 von Java spec erwähnt, und ein Speichermodell ist in §17.4 von Java spec)

Also (in Java) sollte es dich nicht interessieren wann ein Objekt wird gelöscht und du könntest codieren as-if es passiert nicht (durch Argumentation in einer Abstraktion , wo Sie das ignorieren). Natürlich müssen Sie sich um den Speicherverbrauch und die Menge lebender Objekte kümmern, was eine andere Frage ist. In einigen einfachen Fällen (denken Sie an ein "Hallo Welt" -Programm) können Sie beweisen - oder sich selbst davon überzeugen -, dass der zugewiesene Speicher eher klein ist (z. B. weniger als ein Gigabyte), und dann ist Ihnen das überhaupt egal Löschen von Individuum Objekten. In mehr Fällen können Sie sich selbst davon überzeugen, dass die lebenden Objekte (oder erreichbaren Objekte, die eine Obermenge von lebenden Objekten darstellen) niemals eine vernünftige Grenze überschreiten (und dann verlassen Sie sich darauf) GC, aber es ist dir egal, wie und wann die Speicherbereinigung stattfindet. Lesen Sie über Raumkomplexität .

Ich vermute, dass bei mehreren JVM Implementierungen, die ein kurzlebiges Java Programm wie ein Hallo-Welt-Programm ausführen, der Garbage Collector nicht ausgelöst wird Alle und keine Löschung erfolgt. AFAIU, ein solches Verhalten entspricht den zahlreichen Java Spezifikationen.

Die meisten JVM-Implementierungen verwenden generations Kopiertechniken (zumindest für die meisten Java Objekte, die keine Finalisierung oder schwache Referenzen verwenden; Es ist nicht garantiert, dass die Finalisierung in kurzer Zeit erfolgt und verschoben werden kann. Dies ist nur eine hilfreiche Funktion, von der Ihr Code nicht wesentlich abhängen sollte. Dabei ist der Begriff des Löschens eines einzelnen Objekts nicht sinnvoll (da es sich um einen großen Block handelt) Der Speicher, der Speicherzonen für viele Objekte enthält (möglicherweise mehrere Megabyte gleichzeitig), wird gleichzeitig freigegeben.

Wenn die JVM-Spezifikation vorschreibt, dass jedes Objekt genau so schnell wie möglich gelöscht wird (oder einfach das Löschen von Objekten stärker einschränkt), sind effiziente GC-Techniken für Generationen verboten, und die Designer von Java und der JVM haben dies war weise, das zu vermeiden.

Übrigens könnte es möglich sein, dass eine naive JVM, die niemals Objekte löscht und keinen Speicher freigibt, den Spezifikationen (dem Buchstaben, nicht dem Geist) entspricht und in der Praxis mit Sicherheit eine Hallo-Welt-Sache ausführen kann (beachten Sie, dass die meisten winzige und kurzlebige Java -Programme weisen wahrscheinlich nicht mehr als ein paar Gigabyte Speicher zu). Natürlich ist eine solche JVM nicht erwähnenswert und nur eine Spielzeugsache (wie ist this Implementierung von malloc für C). Weitere Informationen finden Sie im Epsilon NoOp GC . Real-Life-JVMs sind sehr komplexe Softwareteile und mischen verschiedene Techniken zur Speicherbereinigung.

Außerdem ist Java nicht dasselbe wie die JVM, und Sie haben Java Implementierungen, die ohne die JVM ausgeführt werden (z. B. vorzeitig Java Compiler, Android-Laufzeit ). In einigen Fällen (meistens akademischen) könnten Sie sich vorstellen (sogenannte "Compilation-Time Garbage Collection" -Techniken), dass ein Java -Programm bei nicht zuweist oder löscht Laufzeit (z. B. weil der Optimierungs-Compiler klug genug war, nur den Aufrufstapel und automatische Variablen zu verwenden).

Warum werden Java Objekte nicht sofort gelöscht, nachdem sie nicht mehr referenziert wurden?

Weil die Spezifikationen Java und JVM dies nicht erfordern.


Lesen Sie das GC-Handbuch für mehr (und die JVM-Spezifikation ). Beachten Sie, dass die Lebendigkeit (oder Nützlichkeit für zukünftige Berechnungen) eines Objekts eine Eigenschaft des gesamten Programms (nicht modular) ist.

Objective-C bevorzugt einen Referenzzähl Ansatz für Speicherverwaltung . Und das hat auch Fallstricke (z. B. muss sich der Objective-C Programmierer um Zirkelverweise kümmern, indem er schwache Verweise erklärt, aber eine JVM behandelt Zirkelverweise in der Praxis gut, ohne dass dies erforderlich ist Aufmerksamkeit vom Java Programmierer).

Es gibt No Silver Bullet in der Programmierung und im Design von Programmiersprachen (beachten Sie das Halting Problem ; ein nützliches lebendes Objekt zu sein ist - nentscheidbar im Allgemeinen).

Sie können auch SICP , Programmiersprache Pragmatik , das Drachenbuch , LISP in kleinen Stücken und Betriebssysteme: Drei einfache Teile . Es geht nicht um Java, aber sie werden Ihren Geist öffnen und helfen zu verstehen, was eine JVM tun sollte und wie sie (mit anderen Teilen) auf Ihrem Computer praktisch funktionieren könnte. Sie können auch viele Monate (oder mehrere Jahre) damit verbringen, den komplexen Quellcode bestehender Open Source JVM-Implementierungen zu studieren (wie OpenJDK , der mehrere Millionen Quellcodezeilen enthält). .

45

Um die Objective-C-Terminologie zu verwenden, sind alle Java - Referenzen von Natur aus "stark".

Das ist nicht richtig - Java hat sowohl schwache als auch weiche Referenzen, obwohl diese eher auf Objektebene als als Sprachschlüsselwörter implementiert sind.

Wenn in Objective-C ein Objekt keine starken Referenzen mehr hat, wird das Objekt sofort gelöscht.

Das ist auch nicht unbedingt richtig - einige Versionen von Objective C verwendeten tatsächlich einen Garbage Collector der Generation. Andere Versionen hatten überhaupt keine Speicherbereinigung.

Es ist richtig, dass neuere Versionen von Objective C anstelle eines Trace-basierten GC die automatische Referenzzählung (ARC) verwenden. Dies führt (häufig) dazu, dass das Objekt "gelöscht" wird, wenn diese Referenzzählung Null erreicht. Beachten Sie jedoch, dass eine JVM-Implementierung auch kompatibel sein und genau so funktionieren kann (zum Teufel, sie kann konform sein und überhaupt keine GC haben.)

Warum tun die meisten JVM-Implementierungen dies nicht und verwenden stattdessen Trace-basierte GC-Algorithmen?

Einfach ausgedrückt ist ARC nicht so utopisch, wie es zunächst scheint:

  • Sie müssen einen Zähler jedes Mal erhöhen oder verringern, wenn eine Referenz kopiert, geändert oder außerhalb des Gültigkeitsbereichs liegt, was einen offensichtlichen Leistungsaufwand mit sich bringt.
  • ARC kann zyklische Referenzen nicht einfach löschen, da sie alle eine Referenz zueinander haben, sodass ihre Referenzanzahl niemals Null erreicht.

ARC hat natürlich Vorteile - seine einfache Implementierung und Erfassung ist deterministisch. Die oben genannten Nachteile sind unter anderem der Grund dafür, dass die meisten JVM-Implementierungen einen generationenbasierten, Trace-basierten GC verwenden.

23
berry120

Java gibt nicht genau an, wann das Objekt erfasst wird, da Implementierungen die Freiheit haben, zu entscheiden, wie mit der Speicherbereinigung umgegangen werden soll.

Es gibt viele verschiedene Speicherbereinigungsmechanismen, aber diejenigen, die garantieren, dass Sie ein Objekt sofort erfassen können, basieren fast ausschließlich auf der Referenzzählung (mir ist kein Algorithmus bekannt, der diesen Trend bricht). Die Referenzzählung ist ein leistungsstarkes Werkzeug, das jedoch mit der Aufrechterhaltung der Referenzzählung verbunden ist. Bei Code mit Singuletthread ist dies nichts anderes als ein Inkrementieren und Dekrementieren. Das Zuweisen eines Zeigers kann also im Referenzzählcode Code in der Größenordnung von 3x so viel kosten wie im Code ohne Referenzzähler (wenn der Compiler alles auf die Maschine zurückbacken kann Code).

Bei Multithread-Code sind die Kosten höher. Es erfordert entweder atomare Inkremente/Dekremente oder Sperren, die beide teuer sein können. Auf einem modernen Prozessor kann eine atomare Operation in der Größenordnung von 20x teurer sein als eine einfache Registeroperation (variiert offensichtlich von Prozessor zu Prozessor). Dies kann die Kosten erhöhen.

Damit können wir die Kompromisse berücksichtigen, die mehrere Modelle eingegangen sind.

  • Objective-C konzentriert sich auf ARC - automatisierte Referenzzählung. Ihr Ansatz besteht darin, die Referenzzählung für alles zu verwenden. Es gibt keine Zykluserkennung (von der ich weiß), daher wird von Programmierern erwartet, dass sie das Auftreten von Zyklen verhindern, was Entwicklungszeit kostet. Ihre Theorie ist, dass Zeiger nicht allzu oft zugewiesen werden und ihr Compiler Situationen identifizieren kann, in denen das Inkrementieren/Dekrementieren von Referenzzählern nicht zum Absterben eines Objekts führen kann, und diese Inkremente/Dekremente vollständig beseitigen kann. Somit minimieren sie die Kosten für die Referenzzählung.

  • CPython verwendet einen Hybridmechanismus. Sie verwenden Referenzzähler, haben aber auch einen Garbage Collector, der Zyklen identifiziert und freigibt. Dies bietet die Vorteile beider Welten auf Kosten beider Ansätze. CPython muss beide Referenzzählungen beibehalten nd Buchführung durchführen, um Zyklen zu erkennen. CPython kommt damit auf zwei Arten davon. Die Faust ist, dass CPython wirklich nicht vollständig multithreaded ist. Es hat eine Sperre, die als GIL bekannt ist und das Multithreading begrenzt. Dies bedeutet, dass CPython normale Inkremente/Dekremente anstelle von atomaren verwenden kann, was viel schneller ist. CPython wird ebenfalls interpretiert, was bedeutet, dass Operationen wie die Zuweisung zu einer Variablen bereits eine Handvoll Anweisungen und nicht nur 1 erfordern. Die zusätzlichen Kosten für das Ausführen der Inkremente/Dekremente, die im C-Code schnell ausgeführt werden, sind weniger problematisch, da wir ' Ich habe diese Kosten bereits bezahlt.

  • Java geht den Ansatz ein, ein System mit Referenzzählung überhaupt nicht zu garantieren. In der Tat sagt die Spezifikation nicht irgendetwas darüber aus, wie Objekte verwaltet werden, außer dass es ein automatisches Speicherverwaltungssystem geben wird. Die Spezifikation weist jedoch auch stark auf die Annahme hin, dass dies Müll ist, der auf eine Weise gesammelt wird, die Zyklen handhabt. Wenn Sie nicht angeben, wann Objekte ablaufen, erhält Java die Freiheit, Kollektoren zu verwenden, die keine Zeit mit Inkrementieren/Dekrementieren verschwenden. In der Tat können clevere Algorithmen wie Müllsammler der Generation sogar viele einfache Fälle behandeln, ohne sie auch nur anzusehen bei den Daten, die zurückgefordert werden (sie müssen nur Daten betrachten, auf die noch verwiesen wird).

Wir können also sehen, dass jeder dieser drei Kompromisse eingehen musste. Welcher Kompromiss am besten ist, hängt stark von der Art und Weise ab, wie die Sprache verwendet werden soll.

5
Cort Ammon

Obwohl finalize auf Javas GC huckepack genommen wurde, interessiert sich die Garbage Collection im Kern nicht für tote Objekte, sondern für lebende. Auf einigen GC-Systemen (möglicherweise einschließlich einiger Implementierungen von Java) kann das einzige, was eine Reihe von Bits, die ein Objekt darstellen, von einer Reihe von Speichern unterscheidet, die für nichts verwendet werden, das Vorhandensein von Verweisen auf die ersteren sein. Während Objekte mit Finalisatoren zu einer speziellen Liste hinzugefügt werden, haben andere Objekte möglicherweise nirgendwo im Universum etwas, das besagt, dass ihr Speicher einem Objekt zugeordnet ist, mit Ausnahme von Referenzen, die im Benutzercode enthalten sind. Wenn die letzte solche Referenz überschrieben wird, wird das Bitmuster im Speicher sofort nicht mehr als Objekt erkennbar sein, unabhängig davon, ob dies im Universum bekannt ist oder nicht.

Der Zweck der Speicherbereinigung besteht nicht darin, Objekte zu zerstören, auf die keine Verweise vorhanden sind, sondern drei Dinge zu erreichen:

  1. Ungültige schwache Referenzen, die Objekte identifizieren, denen keine leicht erreichbaren Referenzen zugeordnet sind.

  2. Durchsuchen Sie die Liste der Objekte des Systems mit Finalisierern, um festzustellen, ob mit diesen Objekten keine leicht erreichbaren Referenzen verknüpft sind.

  3. Identifizieren und konsolidieren Sie Speicherbereiche, die von keinem Objekt verwendet werden.

Beachten Sie, dass das Hauptziel des GC # 3 ist und je länger man wartet, desto mehr Konsolidierungsmöglichkeiten hat man wahrscheinlich. Es ist sinnvoll, # 3 in Fällen auszuführen, in denen der Speicher sofort verwendet werden kann, andernfalls ist es sinnvoller, ihn aufzuschieben.

4
supercat

Lassen Sie mich eine Neuformulierung und Verallgemeinerung Ihrer Frage vorschlagen:

Warum gibt Java keine starken Garantien für den GC-Prozess ab?

Blättern Sie in diesem Sinne kurz durch die Antworten hier. Bisher gibt es sieben (ohne diese), mit einigen Kommentarthreads.

Das ist Ihre Antwort.

GC ist schwer. Es gibt viele Überlegungen, viele verschiedene Kompromisse und letztendlich viele sehr unterschiedliche Ansätze. Einige dieser Ansätze machen es möglich, ein Objekt zu GC zu machen, sobald es nicht benötigt wird. andere nicht. Indem Sie den Vertrag locker halten, Java bietet seinen Implementierern mehr Optionen.

Selbst bei dieser Entscheidung gibt es natürlich einen Kompromiss: Indem der Vertrag locker gehalten wird, nimmt Java meistens * den Programmierern die Möglichkeit, sich auf Destruktoren zu verlassen. Dies ist etwas, das C++ - Programmierer besonders häufig tun miss ([Zitat benötigt];)), es ist also kein unbedeutender Kompromiss. Ich habe keine Diskussion über diese bestimmte Meta-Entscheidung gesehen, aber vermutlich haben die Leute von Java] entschieden, dass die Vorteile von mehr GC-Optionen die Vorteile überwiegen, Programmierern genau zu sagen, wann ein Objekt vorhanden ist wird zerstört werden.


* Es gibt die Methode finalize, aber aus verschiedenen Gründen, die für diese Antwort nicht relevant sind, ist es schwierig und keine gute Idee, sich darauf zu verlassen.

4
yshavit

Es gibt zwei verschiedene Strategien für den Umgang mit Speicher ohne expliziten Code, den der Entwickler geschrieben hat: Garbage Collection und Referenzzählung.

Garbage Collection hat den Vorteil, dass es "funktioniert", es sei denn, der Entwickler tut etwas Dummes. Mit der Referenzzählung können Sie Referenzzyklen haben, was bedeutet, dass es "funktioniert", aber der Entwickler muss manchmal klug sein. Das ist also ein Plus für die Speicherbereinigung.

Bei der Referenzzählung verschwindet das Objekt sofort, wenn die Referenzzählung auf Null sinkt. Das ist ein Vorteil für die Referenzzählung.

Schnell ist die Speicherbereinigung schneller, wenn Sie den Fans der Speicherbereinigung glauben, und die Referenzzählung ist schneller, wenn Sie den Fans der Referenzzählung glauben.

Es sind nur zwei verschiedene Methoden, um das gleiche Ziel zu erreichen: Java hat eine Methode ausgewählt, Objective-C hat eine andere ausgewählt (und viel Compiler-Unterstützung hinzugefügt, um sie von "Pain-in-the-Ass" zu ändern) etwas, das für Entwickler wenig Arbeit ist).

Das Ändern von Java von der Speicherbereinigung zur Referenzzählung wäre ein großes Unterfangen, da viele Codeänderungen erforderlich wären.

Theoretisch hätte Java eine Mischung aus Speicherbereinigung und Referenzzählung implementieren können: Wenn die Referenzanzahl 0 ist, ist das Objekt nicht erreichbar, aber nicht unbedingt umgekehrt. könnte Referenzzähler beibehalten und Objekte löschen, wenn ihr Referenzzähler Null ist (und dann von Zeit zu Zeit eine Speicherbereinigung ausführen, um Objekte innerhalb nicht erreichbarer Referenzzyklen zu fangen). Ich denke, die Welt ist 50/50 in Menschen aufgeteilt, die denken Das Hinzufügen der Referenzzählung zur Garbage Collection ist eine schlechte Idee, und Leute, die das Hinzufügen der Garbage Collection zur Referenzzählung für eine schlechte Idee halten. Das wird also nicht passieren.

Also Java könnte Objekte sofort löschen, wenn ihre Referenzanzahl Null wird, und Objekte innerhalb nicht erreichbarer Zyklen später löschen. Aber das ist eine Entwurfsentscheidung und Java hat sich dagegen entschieden.

3
gnasher729

Alle anderen Leistungsargumente und Diskussionen über die Schwierigkeit des Verstehens, wenn es keine Verweise mehr auf ein Objekt gibt, sind richtig, obwohl eine andere Idee, die ich für erwähnenswert halte, darin besteht, dass es mindestens eine JVM (azul) gibt, die so etwas in Betracht zieht , dass es parallel gc implementiert, das im Wesentlichen einen VM-Thread hat, der ständig Referenzen überprüft, um zu versuchen, sie zu löschen, was sich nicht ganz anders verhält als das, worüber Sie sprechen. Grundsätzlich wird ständig nach dem Heap gesucht und versucht, Speicher zurückzugewinnen, auf den nicht verwiesen wird. Dies verursacht zwar sehr geringe Leistungskosten, führt jedoch zu im Wesentlichen null oder sehr kurzen GC-Zeiten. (Es sei denn, die ständig wachsende Heap-Größe überschreitet das System RAM und dann wird Azul verwirrt und dann gibt es Drachen)

TLDR So etwas gibt es für die JVM, es ist nur eine spezielle JVM und sie hat Nachteile wie jeder andere technische Kompromiss.

Haftungsausschluss: Ich habe keine Verbindung zu Azul, wir haben es gerade bei einem früheren Job verwendet.

1
ford prefect

Die Maximierung des anhaltenden Durchsatzes oder die Minimierung der GC-Latenz stehen unter dynamischer Spannung. Dies ist wahrscheinlich der häufigste Grund, warum GC nicht sofort auftritt. In einigen Systemen, wie z. B. 911-Notfall-Apps, kann das Nichteinhalten eines bestimmten Latenzschwellenwerts dazu führen, dass Site-Failover-Prozesse ausgelöst werden. In anderen Ländern, wie einer Bank- und/oder Arbitrage-Site, ist es weitaus wichtiger, den Durchsatz zu maximieren.

1
barmid

Geschwindigkeit

Warum all dies geschieht, liegt letztendlich an der Geschwindigkeit. Wenn Prozessoren unendlich schnell oder (um praktisch zu sein) nahe daran wären, z. 1.000.000.000.000.000.000.000.000.000.000.000.000 Operationen pro Sekunde, dann können wahnsinnig lange und komplizierte Dinge zwischen den einzelnen Bedienern passieren, z. B. sicherstellen, dass nicht referenzierte Objekte gelöscht werden. Da diese Anzahl von Vorgängen pro Sekunde derzeit nicht zutrifft und es, wie die meisten anderen Antworten erklären, tatsächlich kompliziert und ressourcenintensiv ist, dies herauszufinden, gibt es eine Speicherbereinigung, sodass sich Programme auf das konzentrieren können, was sie tatsächlich in a erreichen möchten schnelle Weise.

0
Michael Durrant