Sobald ein Objekt in Java keine Referenzen mehr hat, kann es 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 hat, wird das Objekt sofort gelöscht. Warum ist das in Java nicht der Fall?
java
garbage-collection
moonman239
quelle
quelle
Antworten:
Erstens verfügt Java über schwache Referenzen und eine weitere Kategorie, die als "weiche Referenzen" bezeichnet wird. Schwache oder starke Referenzen sind ein völlig anderes Thema als Referenzzählung oder Garbage Collection.
Zweitens gibt es Muster in der Speichernutzung, die die Speicherbereinigung durch Platzverlust zeitlich effizienter machen können. Zum Beispiel werden neuere Objekte viel häufiger gelöscht als ältere Objekte. Wenn Sie also zwischen den Sweeps etwas warten, können Sie den größten Teil der neuen Speichergeneration löschen, während Sie die wenigen Überlebenden in einen langfristigen Speicher verschieben. Dieser längerfristige Speicher kann viel seltener gescannt werden. Das sofortige Löschen durch manuelle Speicherverwaltung oder Referenzzählung ist sehr viel anfälliger für Fragmentierung.
Es ist ein bisschen wie der Unterschied, ob man einmal pro Gehaltsscheck einkauft oder jeden Tag nur genug zu essen für einen Tag. Ihre eine große Reise wird viel länger dauern als eine einzelne kleine Reise, aber insgesamt sparen Sie Zeit und wahrscheinlich Geld.
quelle
Weil es nicht einfach ist, zu wissen, auf was nicht mehr verwiesen wird. Nicht mal annähernd so einfach.
Was ist, wenn sich zwei Objekte gegenseitig referenzieren? Bleiben sie für immer? Wenn Sie diese Überlegungen auf das Auflösen beliebiger Datenstrukturen ausweiten, werden Sie bald feststellen, warum die JVM oder andere Garbage Collectors viel ausgefeiltere Methoden anwenden müssen, um zu bestimmen, was noch benötigt wird und was noch möglich ist.
quelle
AFAIK, die JVM-Spezifikation (in englischer Sprache) erwähnt nicht, wann genau ein Objekt (oder ein Wert) gelöscht werden soll, und überlässt dies der Implementierung (ebenfalls für R5RS ). Irgendwie wird ein Garbage Collector benötigt oder vorgeschlagen, die Details bleiben jedoch der Implementierung überlassen. Und ebenfalls für die Java-Spezifikation.
Denken Sie daran , dass Programmiersprachen sind Spezifikationen (von Syntax , Semantik , etc ...), 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 Schwerpunkt liegt bei mir. Die BTW-Finalisierung wird in §12.6 der Java-Spezifikation und ein Speichermodell in §17.4 der Java-Spezifikation erwähnt.)
(In Java) sollte es Ihnen also egal sein, wann ein Objekt gelöscht wird , und Sie könnten so codieren, als ob dies nicht der Fall wäre (indem Sie in einer Abstraktion argumentieren, in der Sie dies ignorieren). Natürlich müssen Sie sich um den Speicherverbrauch und den Satz lebender Objekte kümmern, was eine andere Frage ist. In einigen einfachen Fällen (denken Sie an ein "Hallo-Welt" -Programm) können Sie nachweisen - oder sich selbst davon überzeugen -, dass der zugewiesene Speicher ziemlich klein ist (z. B. weniger als ein Gigabyte), und dann interessiert Sie das überhaupt nicht Löschen einzelner Objekte. In mehr Fällen können Sie sich davon überzeugen, dass die lebenden Objekte(oder erreichbare, was eine Menge ist, die es einfacher macht, über lebende nachzudenken) Überschreiten Sie niemals eine vernünftige Grenze (und dann verlassen Sie sich auf GC, aber es ist Ihnen egal, wie und wann die Garbage Collection stattfindet). Lesen Sie mehr über Raumkomplexität .
Ich vermute, dass bei einigen JVM- Implementierungen, in denen ein kurzlebiges Java-Programm wie ein Hello World-Programm ausgeführt wird, der Garbage Collector überhaupt nicht ausgelöst wird und keine Löschung erfolgt. AFAIU, ein solches Verhalten entspricht den zahlreichen Java-Spezifikationen.
Die meisten JVM-Implementierungen verwenden generative Kopiertechniken (zumindest für die meisten Java-Objekte, die keine Finalisierung oder schwachen Referenzen verwenden) . Die Finalisierung kann nicht garantiert in kurzer Zeit erfolgen und kann verschoben werden. Dies ist nur eine hilfreiche Funktion, die Ihr Code nicht sollte davon abhängen, inwieweit der Gedanke, ein einzelnes Objekt zu löschen, keinen Sinn ergibt (da ein großer Block von Speicherbereichen für viele Objekte, möglicherweise mehrere Megabyte auf einmal, auf einmal freigegeben wird).
Wenn die JVM-Spezifikation vorschreibt, dass jedes Objekt so schnell wie möglich genau gelöscht wird (oder einfach die Objektlöschung stärker einschränkt), sind effiziente generative GC-Techniken verboten, und die Designer von Java und der JVM haben dies mit Bedacht vermieden.
Übrigens könnte es sein, dass eine naive JVM, die niemals Objekte löscht und keinen Speicher freigibt, den Spezifikationen (dem Buchstaben, nicht dem Geist) entspricht und mit Sicherheit in der Lage ist, in der Praxis eine Hallo-Welt-Sache auszuführen (beachten Sie, dass die meisten winzige und kurzlebige Java-Programme belegen wahrscheinlich nicht mehr als ein paar Gigabyte Speicher. Natürlich ist eine solche JVM nicht erwähnenswert und nur eine Spielzeugsache (wie diese Implementierung
malloc
für C). Weitere Informationen finden Sie im Epsilon NoOp GC . Real-Life-JVMs sind sehr komplexe Softwareteile und kombinieren verschiedene Techniken zur Garbage Collection.Auch Java ist nicht die gleiche wie die JVM, und Sie haben Java - Implementierungen ohne die JVM ausgeführt wird (zB Voraus-Zeit Java - Compiler, Android Runtime ). In einigen Fällen (meistens akademischen) können Sie sich vorstellen (sogenannte "Kompilierungszeit-Garbage-Collection" -Techniken), dass ein Java-Programm zur Laufzeit keine Zuordnung oder Löschung vornimmt (z. B. weil der optimierende Compiler klug genug war, nur die zu verwenden) Aufrufstapel und automatische Variablen ).
Weil die Java- und JVM-Spezifikationen dies nicht erfordern.
Lesen Sie das GC-Handbuch (und die JVM-Spezifikation ). Beachten Sie, dass es sich bei einem Objekt um eine (nicht modulare) Ganzprogramm-Eigenschaft handelt, die lebendig ist (oder für zukünftige Berechnungen nützlich ist).
Objective-C bevorzugt einen Referenzzählansatz für die Speicherverwaltung . Und das hat auch Fallstricke (z. B. muss sich der Objective-C- Programmierer um Zirkelverweise kümmern, indem er schwache Verweise expliziert, aber eine JVM verarbeitet Zirkelverweise in der Praxis gut, ohne dass der Java-Programmierer darauf achten muss).
Es gibt kein Patentrezept für die Programmierung und das Design von Programmiersprachen (seien Sie sich des Halteproblems bewusst , ein nützliches lebendes Objekt zu sein, ist im Allgemeinen unentscheidbar ).
Sie könnten auch SICP , Programmiersprache Pragmatik , das Drachenbuch , Lisp in kleinen Stücken und Betriebssysteme lesen : Drei einfache Stücke . Es geht nicht um Java, aber es wird Ihnen den Kopf öffnen und Ihnen helfen, zu verstehen, was eine JVM tun sollte und wie sie (mit anderen Komponenten) auf Ihrem Computer praktisch funktionieren könnte. Sie könnten auch viele Monate (oder mehrere Jahre) damit verbringen, den komplexen Quellcode bestehender Open-Source- JVM-Implementierungen (wie OpenJDK mit mehreren Millionen Quellcodezeilen) zu untersuchen.
quelle
finalize
bei der Ressourcenverwaltung (von Dateihandles, DB-Verbindungen, GPU-Ressourcen usw.) nicht mehr darauf verlassen können.Das ist nicht richtig - Java hat sowohl schwache als auch weiche Referenzen, obwohl diese eher auf Objektebene als als Sprachschlüsselwörter implementiert werden.
Das ist auch nicht unbedingt richtig - einige Versionen von Objective C verwendeten in der Tat einen generationsübergreifenden Garbage Collector. Andere Versionen hatten überhaupt keine Speicherbereinigung.
Es ist richtig, dass neuere Versionen von Objective C die automatische Referenzzählung (ARC) anstelle einer Trace-basierten GC 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 könnte kompatibel sein und überhaupt keinen GC haben).
Warum tun dies die meisten JVM-Implementierungen nicht und verwenden stattdessen Trace-basierte GC-Algorithmen?
Einfach ausgedrückt ist ARC nicht so utopisch, wie es zunächst scheint:
ARC hat natürlich Vorteile - es ist einfach zu implementieren und die Erfassung ist deterministisch. Die oben genannten Nachteile sind jedoch unter anderem der Grund, warum die Mehrheit der JVM-Implementierungen eine generationsbasierte Ablaufverfolgungs-GC verwendet.
quelle
Java gibt nicht genau an, wann das Objekt erfasst wird, da Implementierungen die Freiheit haben, zu entscheiden, wie mit der Garbage Collection 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). Referenzzählung ist ein leistungsfähiges Werkzeug, das jedoch die Aufrechterhaltung der Referenzzählung kostet. In Singlethread-Code ist das nichts anderes als ein Inkrementieren und Dekrementieren. Das Zuweisen eines Zeigers kann also Kosten in der Größenordnung des 3-fachen des Referenzzählcodes kosten, als dies bei nicht-Referenzzählcode der Fall ist (wenn der Compiler alles auf den Computer zurückspielen kann) Code).
Bei Multithread-Code sind die Kosten höher. Entweder werden atomare Inkremente / Dekremente oder Sperren benötigt, was teuer sein kann. Auf einem modernen Prozessor kann eine atomare Operation in der Größenordnung von 20x teurer sein als eine einfache Registeroperation (offensichtlich variiert sie von Prozessor zu Prozessor). Dies kann die Kosten erhöhen.
Wir können also die Kompromisse berücksichtigen, die mehrere Modelle eingehen.
Objective-C konzentriert sich auf ARC - automatisierte Referenzzählung. Ihr Ansatz ist es, die Referenzzählung für alles zu verwenden. Es gibt keine Zykluserkennung (die ich kenne), daher wird von Programmierern erwartet, dass sie das Auftreten von Zyklen verhindern, was die 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ählwerten nicht zum Absterben eines Objekts führen kann, und diese Inkrementierungen / Dekrementierungen vollständig beseitigen kann. Somit minimieren sie die Kosten für die Referenzzählung.
CPython verwendet einen Hybridmechanismus. Sie verwenden Referenzzählungen, haben aber auch einen Garbage Collector, der Zyklen identifiziert und freigibt. Dies bietet die Vorteile beider Welten auf Kosten beider Ansätze. CPython müssen beide Referenzzähler halten undFühren Sie die Buchführung durch, um Zyklen zu erkennen. CPython schafft das auf zwei Arten. Die Faust ist, dass CPython wirklich nicht vollständig multithreaded ist. Es gibt eine Sperre namens GIL, die Multithreading einschränkt. Dies bedeutet, dass CPython normale Inkremente / Dekremente anstelle von atomaren verwenden kann, was viel schneller ist. CPython wird auch interpretiert, was bedeutet, dass Vorgänge wie die Zuweisung zu einer Variablen bereits eine Handvoll Anweisungen erfordern und nicht nur 1. Die zusätzlichen Kosten für die Durchführung der Inkremente / Dekremente, die im C-Code schnell durchgeführt werden, sind weniger problematisch, da wir haben diese kosten schon bezahlt.
Java geht den Ansatz ein, überhaupt kein Referenzzählsystem zu garantieren. In der Tat sagt die Spezifikation nichts 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, kann Java Kollektoren verwenden, die keine Zeit für das Inkrementieren / Dekrementieren verschwenden. In der Tat können clevere Algorithmen wie Müllsammler der Generation sogar viele einfache Fälle behandeln, ohne sich die Daten anzusehen, die zurückgefordert werden (sie müssen nur die Daten ansehen, auf die noch verwiesen wird).
So können wir sehen, dass jeder dieser drei Kompromisse eingehen musste. Welcher Kompromiss am besten ist, hängt stark davon ab, wie die Sprache verwendet werden soll.
quelle
Obwohl
finalize
auf Javas GC huckepack genommen, ist die Müllabfuhr im Kern nicht an toten Objekten interessiert, sondern an lebenden. Auf einigen GC-Systemen (möglicherweise einschließlich einiger Java-Implementierungen) kann das einzige Unterscheidungsmerkmal zwischen einer Reihe von Bits, die ein Objekt darstellen, und einer Reihe von Speichern, die für nichts verwendet werden, das Vorhandensein von Verweisen auf die ersten sein. Während Objekte mit Finalisierern 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, das Bitmuster im Speicher wird sofort aufhören , als ein Objekt erkennbar zu sein, ob oder ob nicht irgendetwas im Universum mir bewusst ist.Der Zweck der Garbage Collection besteht nicht darin, Objekte zu zerstören, auf die nicht verwiesen wird, sondern drei Dinge zu erreichen:
Ungültige schwache Referenzen, die Objekte identifizieren, denen keine stark erreichbaren Referenzen zugeordnet sind.
Durchsuchen Sie die Liste der Objekte des Systems mit Finalisierern, um festzustellen, ob mit diesen Objekten keine stark erreichbaren Referenzen verknüpft sind.
Identifizieren und konsolidieren Sie Speicherbereiche, die nicht von Objekten verwendet werden.
Beachten Sie, dass das primäre Ziel des GC # 3 ist und je länger man darauf wartet, desto mehr Möglichkeiten bei der Konsolidierung werden sich bieten. Es ist sinnvoll, Nummer 3 zu verwenden, wenn die Speicherung sofort verwendet werden soll, andernfalls ist es sinnvoller, sie aufzuschieben.
quelle
Lassen Sie mich eine Umformulierung und Verallgemeinerung Ihrer Frage vorschlagen:
In diesem Sinne blättern Sie hier kurz durch die Antworten. Bisher gibt es sieben (ohne diesen) mit einigen Kommentarthreads.
Das ist deine 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, sobald es nicht benötigt wird; andere nicht. Indem Java den Vertrag locker hält, bietet es seinen Implementierern mehr Optionen.
Selbst in dieser Entscheidung liegt ein Kompromiss: Indem Java den Vertrag locker hält, wird Programmierern die Möglichkeit genommen, sich auf Destruktoren zu verlassen. Dies ist etwas, was insbesondere C ++ - Programmierer oft übersehen ([Zitieren benötigt];)), es ist also kein unbedeutender Kompromiss. Ich habe noch keine Diskussion über diese spezielle Meta-Entscheidung gesehen, aber vermutlich haben die Java-Leute entschieden, dass die Vorteile einer größeren Anzahl von GC-Optionen die Vorteile überwiegen, wenn Programmierer genau wissen, wann ein Objekt zerstört wird.
* Es gibt die
finalize
Methode, aber aus verschiedenen Gründen, die für diese Antwort nicht in Frage kommen, ist es schwierig und keine gute Idee, sich darauf zu verlassen.quelle
Es gibt zwei verschiedene Strategien für den Umgang mit Speicher ohne expliziten Code, der vom Entwickler geschrieben wurde: Speicherbereinigung und Referenzzählung.
Garbage Collection hat den Vorteil, dass es "funktioniert", es sei denn, der Entwickler macht etwas Dummes. Mit der Referenzzählung können Sie Referenzzyklen erstellen, was bedeutet, dass es "funktioniert", aber der Entwickler muss manchmal schlau sein. Das ist also ein Plus für die Müllabfuhr.
Bei der Referenzzählung verschwindet das Objekt sofort, wenn der Referenzzähler auf Null abfällt. Das ist ein Vorteil für die Referenzzählung.
Geschwindigkeitsmäßig 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 eine andere (und es wurde eine Menge Compiler-Unterstützung hinzugefügt, um es von einem "Pain-in-the-Ass" in etwas zu verwandeln, das für Entwickler wenig Arbeit bedeutet).
Die Umstellung von Java von der Garbage Collection auf die Referenzzählung wäre ein großes Unterfangen, da viele Codeänderungen erforderlich wären.
Theoretisch hätte Java eine Mischung aus Garbage Collection und Referenzzählung implementieren können: Wenn die Referenzzählung 0 ist, ist das Objekt nicht erreichbar, aber nicht unbedingt umgekehrt. Sie können also die Referenzanzahl beibehalten und Objekte löschen, wenn ihre Referenzanzahl Null ist (und dann von Zeit zu Zeit die Garbage Collection ausführen, um Objekte innerhalb nicht erreichbarer Referenzzyklen abzufangen). Ich denke, die Welt ist 50/50 gespalten in Menschen, die denken, dass es eine schlechte Idee ist, die Speicherbereinigung um die Referenzzählung zu erweitern, und Menschen, die denken, dass es eine schlechte Idee ist, die Speicherbereinigung um die Referenzzählung zu erweitern. Das wird also nicht passieren.
So könnte Java Objekte sofort löschen, wenn ihre Referenzanzahl Null wird, und Objekte innerhalb nicht erreichbarer Zyklen später löschen. Aber das ist eine Designentscheidung, und Java hat sich dagegen entschieden.
quelle
Alle anderen Leistungsargumente und Diskussionen über die Schwierigkeit des Verstehens, wenn es keine Verweise mehr auf ein Objekt gibt, sind korrekt, obwohl eine andere Idee, die meiner Meinung nach erwähnenswert ist, darin besteht, dass es mindestens eine JVM (azul) gibt, die so etwas in Betracht zieht , dass es parallele gc implementiert, die im Wesentlichen einen vm-Thread hat, der ständig die Referenzen überprüft, um zu versuchen, sie zu löschen, was nicht ganz anders ist als das, worüber Sie sprechen. Grundsätzlich wird der Heap ständig überprüft und versucht, alle nicht referenzierten Speicher zurückzugewinnen. Dies ist mit sehr geringen Leistungskosten verbunden, führt jedoch zu im Wesentlichen null oder sehr kurzen GC-Zeiten. (Es sei denn, die ständig wachsende Größe des Heapspeichers überschreitet den Arbeitsspeicher des Systems und dann ist Azul verwirrt und es gibt 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 Beziehung zu Azul, wir haben es gerade bei einem früheren Job verwendet.
quelle
Die Maximierung des anhaltenden Durchsatzes oder die Minimierung der GC-Latenz sind unter dynamischer Spannung, was wahrscheinlich der häufigste Grund dafür ist, dass GC nicht sofort auftritt. In einigen Systemen wie 911-Notfall-Apps kann das Nichteinhalten eines bestimmten Latenzschwellenwerts dazu führen, dass Site-Failover-Prozesse ausgelöst werden. Bei anderen, wie beispielsweise einer Bank- und / oder Arbitrage-Site, ist es weitaus wichtiger, den Durchsatz zu maximieren.
quelle
Geschwindigkeit
Warum das alles so ist, liegt letztendlich an der Geschwindigkeit. Wenn Prozessoren unendlich schnell waren oder (um praktisch zu sein) in der Nähe waren, z. B. 1.000.000.000.000.000.000.000.000.000.000 pro Sekunde, kann es zu wahnsinnig langen und komplizierten Vorgängen zwischen den einzelnen Operatoren kommen, z. B. um sicherzustellen, 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, kompliziert und ressourcenintensiv ist, dies herauszufinden, gibt es eine Speicherbereinigung, damit sich Programme auf das konzentrieren können, was sie tatsächlich in a erreichen möchten schnelle Art und Weise.
quelle