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


77

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?


46
Es sollte Sie nicht interessieren, wann Java-Objekte tatsächlich gelöscht werden. Es ist ein Implementierungsdetail.
Basile Starynkevitch

154
@BasileStarynkevitch Sie sollten sich unbedingt darum kümmern und herausfordern, wie Ihr System / Ihre Plattform funktioniert. Fragen zu stellen, wie und warum, ist eine der besten Möglichkeiten, um ein besserer Programmierer (und im Allgemeinen eine intelligentere Person) zu werden.
Artur Biesiadowski

6
Was macht Objective C bei Zirkelverweisen? Ich nehme an, es leckt sie nur?
Mehrdad

45
@ArturBiesiadowksi: Nein, die Java-Spezifikation gibt nicht an, wann ein Objekt gelöscht wird (und auch nicht für R5RS ). Sie könnten und sollten Ihr Java-Programm wahrscheinlich so entwickeln, als ob diese Löschung niemals stattfinden würde (und für kurzlebige Prozesse wie eine Java-Hallo-Welt ist dies in der Tat nicht der Fall). Sie interessieren sich vielleicht für die Menge der lebenden Objekte (oder den Speicherverbrauch), was eine andere Geschichte ist.
Basile Starynkevitch

28
Eines Tages sagte der Novize zum Meister: "Ich habe eine Lösung für unser Zuweisungsproblem. Wir werden jeder Zuweisung eine Referenzanzahl geben, und wenn sie Null erreicht, können wir das Objekt löschen." Der Meister antwortete: "Eines Tages sagte der Novize zum Meister" Ich habe eine Lösung ...
Eric Lippert

Antworten:


79

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.


58
Die Frau eines Programmierers schickt ihn in den Supermarkt. Sie sagt zu ihm: "Kaufen Sie ein Brot, und wenn Sie Eier sehen, schnappen Sie sich ein Dutzend." Der Programmierer kehrt später mit einem Dutzend Broten unter dem Arm zurück.
Neil

7
Ich schlage vor zu erwähnen, dass die GC-Zeit der neuen Generation im Allgemeinen proportional zur Menge der lebenden Objekte ist. Wenn also mehr Objekte gelöscht werden, werden ihre Kosten in vielen Fällen überhaupt nicht bezahlt. Löschen ist so einfach wie das Kippen des Zeigers für den Überlebensraum und optional das Nullsetzen des gesamten Speicherplatzes in einem großen Memset (nicht sicher, ob dies am Ende von gc erfolgt oder während der Zuweisung von Tabellen oder Objekten selbst in aktuellen JVMS amortisiert wird)
Artur Biesiadowski,

64
@Neil sollte das nicht 13 Brote sein?
JAD

67
"Aus um einen Fehler in Gang 7"
joeytwiddle

13
@JAD Ich hätte 13 gesagt, aber die meisten neigen nicht dazu, das zu verstehen. ;)
Neil

86

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.


7
Sie können auch einen Python-Ansatz wählen, bei dem Sie so oft wie möglich die Nachzählung verwenden und auf einen GC zurückgreifen, wenn Sie erwarten, dass zirkuläre Abhängigkeiten Speicher verlieren. Ich verstehe nicht, warum sie nicht zusätzlich zu GC nachzählen konnten?
Mehrdad

27
@Mehrdad Sie könnten. Aber wahrscheinlich wäre es langsamer. Nichts hindert Sie daran, dies zu implementieren, aber erwarten Sie nicht, einen der GCs in Hotspot oder OpenJ9 zu schlagen.
Josef

21
@ jpmc26 denn wenn Sie Objekte löschen, sobald sie nicht mehr verwendet werden, ist die Wahrscheinlichkeit hoch, dass Sie sie in einer Situation mit hoher Auslastung löschen, die die Auslastung noch weiter erhöht. GC kann ausgeführt werden, wenn weniger Last vorhanden ist. Die Referenzzählung selbst ist ein geringer Aufwand für jede Referenz. Auch mit einem GC können Sie häufig einen großen Teil des Speichers ohne Referenzen verwerfen, ohne die einzelnen Objekte zu behandeln.
Josef

33
@ Josef: Richtige Referenzzählung ist auch nicht kostenlos; Die Aktualisierung der Referenzanzahl erfordert atomare Inkremente / Dekremente, die insbesondere bei modernen Multicore-Architekturen überraschend teuer sind. In CPython ist dies kein großes Problem (CPython ist für sich genommen extrem langsam und die GIL beschränkt die Multithread-Leistung auf Single-Core-Ebenen), in einer schnelleren Sprache, die auch Parallelität unterstützt, kann dies jedoch ein Problem sein. Es ist keine Chance, dass PyPy die Referenzzählung vollständig aufgibt und nur GC verwendet.
Matteo Italia

10
@Mehrdad Sobald Sie Ihren Referenzzähl-GC für Java implementiert haben, werde ich ihn gerne testen, um einen Fall zu finden, in dem er schlechter abschneidet als jede andere GC-Implementierung.
Josef

45

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 Heapspeicher für Objekte wird von einem automatischen Speicherverwaltungssystem (Garbage Collector) zurückgefordert. Objekte werden niemals explizit freigegeben. Die Java Virtual Machine setzt keinen bestimmten Typ eines automatischen Speicherverwaltungssystems voraus

(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 mallocfü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 ).

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

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.


20
"Es ist möglich, dass eine naive JVM, die niemals Objekte löscht und keinen Speicher freigibt, den Spezifikationen entspricht." Java 11 fügt tatsächlich einen No-Op-Garbage-Collector für unter anderem sehr kurzlebige Programme hinzu.
Michael

6
"Es sollte Sie nicht interessieren, wann ein Objekt gelöscht wird". Zum einen sollten Sie wissen, dass RAII kein realisierbares Muster mehr ist und dass Sie sich finalizebei der Ressourcenverwaltung (von Dateihandles, DB-Verbindungen, GPU-Ressourcen usw.) nicht mehr darauf verlassen können.
Alexander

4
@Michael Es ist perfekt für die Stapelverarbeitung mit einer verbrauchten Speicherdecke. Das Betriebssystem kann nur sagen "Der gesamte von diesem Programm verwendete Speicher ist jetzt weg!" Immerhin ist das ziemlich schnell. In der Tat wurden viele Programme in C auf diese Weise geschrieben, insbesondere in der frühen Unix-Welt. Pascal hatte den wunderbaren Fehler "Setzen Sie den Stack / Heap-Zeiger auf einen gespeicherten Checkpoint zurück", der es Ihnen ermöglichte, fast dasselbe zu tun, obwohl es ziemlich unsicher war - markieren Sie, starten Sie eine Unteraufgabe, setzen Sie zurück.
Luaan

6
@Alexander im Allgemeinen außerhalb von C ++ (und ein paar Sprachen, die absichtlich davon abgeleitet sind) ist die Annahme, dass RAII ausschließlich auf Finalisierern basiert, ein Anti-Pattern, vor dem gewarnt und durch einen expliziten Ressourcensteuerungsblock ersetzt werden sollte. Der springende Punkt bei GC ist, dass Lebensdauer und Ressourcen schließlich entkoppelt sind.
Leushenko

3
@Leushenko Ich würde stark widersprechen, dass "Lebensdauer und Ressourcen entkoppelt sind" der "springende Punkt" der GC ist. Dies ist der negative Preis, den Sie für den Hauptpunkt von GC zahlen: einfache und sichere Speicherverwaltung. "Die Annahme, dass RAII nur auf der Basis von Finalisierern funktioniert, ist ein Anti-Pattern" In Java? Vielleicht. Aber nicht in CPython, Rust, Swift oder Objective C. "Nein, diese sind streng eingeschränkt." Ein Objekt, das eine Ressource über RAII verwaltet, gibt Ihnen die Möglichkeit, die Lebensdauer des Bereichs weiterzugeben. Ein Try-with-Resource-Block ist auf einen einzelnen Bereich beschränkt.
Alexander

23

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 werden.

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 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:

  • Sie müssen einen Zähler jedes Mal inkrementieren oder dekrementieren, wenn eine Referenz kopiert oder geändert wird oder den Gültigkeitsbereich verlässt, was einen offensichtlichen Mehraufwand für die Leistung mit sich bringt.
  • ARC kann zyklische Referenzen nicht einfach löschen, da sie alle eine Referenz zueinander haben, sodass ihre Referenzzahl niemals Null erreicht.

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.


1
Das Komische ist, dass Apple auf ARC umgestiegen ist, weil es in der Praxis andere GCs (insbesondere Generationen-GCs) bei weitem übertrifft. Um fair zu sein, trifft dies hauptsächlich auf speicherbeschränkten Plattformen (iPhone) zu. Aber ich würde Ihrer Aussage, dass ARC nicht so utopisch ist, wie es zunächst scheint, entgegentreten, indem ich sage, dass generationsbezogene (und andere nicht deterministische) GCs nicht so utopisch sind, wie sie zunächst scheinen: Deterministische Zerstörung ist wahrscheinlich eine bessere Option in der überwiegende Mehrheit der Szenarien.
Konrad Rudolph

3
@KonradRudolph Auch wenn ich eher ein Fan deterministischer Zerstörung bin, denke ich nicht, dass eine bessere Option in den allermeisten Szenarien Bestand hat. Dies ist sicherlich eine bessere Option, wenn Latenz oder Speicher wichtiger sind als der durchschnittliche Durchsatz, und insbesondere, wenn die Logik relativ einfach ist. Es ist jedoch nicht so, dass es nicht viele komplexe Anwendungen gibt, die viele zyklische Verweise usw. erfordern und einen schnellen Durchschnittsbetrieb erfordern. Die Latenz ist jedoch unerheblich, und es steht ausreichend Speicher zur Verfügung. Für diese ist es zweifelhaft, ob ARC eine gute Idee ist.
linksum

1
@leftaroundabout In den meisten Szenarien stellen weder Durchsatz noch Speicherdruck einen Engpass dar, daher spielt es keine Rolle, wie auch immer. Ihr Beispiel ist ein bestimmtes Szenario. Zugegeben, es ist nicht ungewöhnlich, aber ich würde nicht so weit gehen zu behaupten, dass es häufiger ist als andere Szenarien, in denen ARC besser geeignet ist. Darüber hinaus kann ARC gut mit Zyklen umgehen. Es sind nur einige einfache manuelle Eingriffe des Programmierers erforderlich. Dies macht es weniger ideal, aber kaum ein Deal Breaker. Ich behaupte, dass deterministische Finalisierung ein viel wichtigeres Merkmal ist, als Sie vorgeben.
Konrad Rudolph

3
@KonradRudolph Wenn ARC einige einfache manuelle Eingriffe des Programmierers erfordert, werden Zyklen nicht behandelt. Wenn Sie häufig doppelt verknüpfte Listen verwenden, wird ARC für die manuelle Speicherzuweisung verwendet. Wenn Sie über große beliebige Diagramme verfügen, werden Sie von ARC gezwungen, einen Garbage Collector zu schreiben. Das GC-Argument wäre, dass Ressourcen, die zerstört werden müssen, nicht die Aufgabe des Speichersubsystems sind, und um die relativ wenigen zu verfolgen, sollten sie durch einfache manuelle Eingriffe des Programmierers deterministisch finalisiert werden.
Prosfilaes

2
@KonradRudolph ARC und Zyklen führen grundsätzlich zu Speicherlecks, wenn sie nicht manuell behandelt werden. In Systemen mit ausreichender Komplexität können größere Lecks auftreten, wenn z. B. ein in einer Karte gespeichertes Objekt einen Verweis auf diese Karte enthält. Diese Änderung kann von einem Programmierer vorgenommen werden, der nicht für die Codeabschnitte verantwortlich ist, die diese Karte erstellen und zerstören. Große willkürliche Graphen bedeuten nicht, dass die internen Zeiger nicht stark genug sind, damit die verknüpften Elemente verschwinden. Ob der Umgang mit einigen Speicherlecks weniger problematisch ist als das manuelle Schließen von Dateien, möchte ich nicht sagen, aber es ist real.
Prosfilaes

5

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.


4

Obwohl finalizeauf 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:

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

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

  3. 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.


5
Eigentlich hat gc nur ein Ziel: Unendlichen Speicher simulieren. Alles, was Sie als Ziel benannt haben, ist entweder eine Unvollkommenheit in der Abstraktion oder ein Implementierungsdetail.
Deduplikator

@Deduplicator: Schwache Referenzen bieten nützliche Semantik, die ohne GC-Unterstützung nicht erreicht werden kann.
Supercat

Sicher, schwache Referenzen haben eine nützliche Semantik. Aber würde diese Semantik benötigt, wenn die Simulation besser wäre?
Deduplikator

@ Deduplicator: Ja. Stellen Sie sich eine Sammlung vor, die definiert, wie Updates mit der Aufzählung interagieren. Eine solche Sammlung muss möglicherweise schwache Verweise auf Live-Enumeratoren enthalten. In einem System mit unbegrenztem Speicher würde die Liste der interessierten Enumeratoren einer Sammlung, die wiederholt durchlaufen wurde, unbegrenzt wachsen. Der für diese Liste erforderliche Speicher wäre kein Problem, aber die zum Durchlaufen erforderliche Zeit würde die Systemleistung beeinträchtigen. Das Hinzufügen von GC kann den Unterschied zwischen einem O (N) - und einem O (N ^ 2) -Algorithmus bedeuten.
Supercat

2
Warum sollten Sie die Enumeratoren benachrichtigen, anstatt sie an eine Liste anzuhängen und sie sich selbst suchen zu lassen, wenn sie verwendet werden? Und jedes Programm, das davon abhängt, dass Müll zeitnah verarbeitet wird, anstatt vom Speicherdruck abhängig zu sein, lebt ohnehin in einem Zustand der Sünde, wenn es sich überhaupt bewegt.
Deduplicator

4

Lassen Sie mich eine Umformulierung und Verallgemeinerung Ihrer Frage vorschlagen:

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

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 finalizeMethode, 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.


3

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.


Beim Referenzzählen ist das Finalisieren trivial, da der Programmierer sich um die Zyklen kümmert. Mit gc sind Zyklen trivial, aber der Programmierer muss beim Finalisieren vorsichtig sein.
Deduplikator

@Deduplicator In Java, es ist auch möglich , starke Referenzen auf Objekte zu erstellen finalisiert werden ... In Objective-C und Swift, sobald der Referenzzähler Null ist, das Objekt wird verschwinden (es sei denn Du eine Endlosschleife in dealloc / Deist setzen).
gnasher729

Gerade bemerkt, dumme Rechtschreibprüfung ersetzt deinit mit deist ...
gnasher729

1
Es gibt einen Grund, warum die meisten Programmierer die automatische Rechtschreibkorrektur hassen ... ;-)
Deduplizierer

lol ... Ich denke, die Welt ist gespalten zwischen 0,1 / 0,1 / 99,8 Menschen, die denken, dass das Hinzufügen von Referenzzählungen zur Speicherbereinigung eine schlechte Idee ist, und Menschen, die denken, dass das Hinzufügen von Speicherbereinigungen zur Speicherbereinigung eine schlechte Idee ist
Zähle noch

1

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.


1

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.


0

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.


Nun, ich bin sicher, wir würden interessantere Wege finden, um die zusätzlichen Zyklen zu nutzen.
Deduplikator
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.