Der Garbage-Collection-Mechanismus der Java-Plattform hat die Entwicklereffizienz erheblich verbessert, ein schlecht implementierter Garbage Collector kann jedoch zu viele Anwendungsressourcen verbrauchen. Im dritten Teil der Reihe zur Leistungsoptimierung virtueller Java-Maschinen führt Eva Andreasson Java-Anfänger in das Speichermodell und den Garbage-Collection-Mechanismus der Java-Plattform ein. Sie erklärt, warum Fragmentierung (nicht Garbage Collection) das Hauptproblem bei der Leistung von Java-Anwendungen ist und warum Generations-Garbage Collection und Komprimierung derzeit die wichtigsten (aber nicht die innovativsten) Möglichkeiten sind, mit der Fragmentierung von Java-Anwendungen umzugehen.
Der Zweck der Garbage Collection (GC) besteht darin, den von Java-Objekten belegten Speicher freizugeben, auf den keine aktiven Objekte mehr verweisen. Sie ist der Kernbestandteil des dynamischen Speicherverwaltungsmechanismus der Java Virtual Machine. Während eines typischen Garbage-Collection-Zyklus bleiben alle Objekte erhalten, auf die noch verwiesen wird (und die daher erreichbar sind), während diejenigen, auf die nicht mehr verwiesen wird, freigegeben und der von ihnen belegte Speicherplatz für die Zuweisung an neue Objekte zurückgewonnen wird.
Um den Garbage-Collection-Mechanismus und die verschiedenen Garbage-Collection-Algorithmen zu verstehen, müssen Sie zunächst etwas über das Speichermodell der Java-Plattform wissen.
Garbage Collection und das Speichermodell der Java-Plattform
Wenn Sie ein Java-Programm über die Befehlszeile starten und den Startparameter -Xmx angeben (z. B. java -Xmx:2g MyApp), wird der Speicher der angegebenen Größe dem Java-Prozess zugewiesen, dem sogenannten Java-Heap . Dieser dedizierte Speicheradressraum wird zum Speichern von Objekten verwendet, die von Java-Programmen (und manchmal auch der JVM) erstellt wurden. Während die Anwendung ausgeführt wird und kontinuierlich Speicher für neue Objekte zuweist, füllt sich der Java-Heap (d. h. der dedizierte Speicheradressraum) langsam.
Irgendwann wird der Java-Heap voll sein, was bedeutet, dass der Speicherzuweisungsthread keinen zusammenhängenden Speicherplatz finden kann, der groß genug ist, um Speicher für das neue Objekt zuzuweisen. Zu diesem Zeitpunkt beschließt die JVM, den Garbage Collector zu benachrichtigen und die Garbage Collection zu starten. Die Garbage Collection kann auch durch den Aufruf von System.gc() im Programm ausgelöst werden, die Verwendung von System.gc() garantiert jedoch nicht, dass die Garbage Collection durchgeführt wird. Vor jeder Garbage Collection ermittelt der Garbage Collection-Mechanismus zunächst, ob die Durchführung der Garbage Collection sicher ist. Wenn sich alle aktiven Threads der Anwendung an einem sicheren Punkt befinden, kann eine Garbage Collection gestartet werden. Beispielsweise kann die Garbage Collection nicht durchgeführt werden, wenn einem Objekt Speicher zugewiesen wird, oder die Garbage Collection kann nicht durchgeführt werden, während CPU-Anweisungen optimiert werden, da der Kontext wahrscheinlich verloren geht und das Endergebnis falsch ist.
Der Garbage Collector kann kein Objekt mit aktiven Referenzen zurückfordern, was gegen die Java Virtual Machine-Spezifikation verstoßen würde. Es besteht keine Notwendigkeit, tote Objekte sofort zu recyceln, da tote Objekte schließlich durch die anschließende Müllsammlung recycelt werden. Obwohl es viele Möglichkeiten gibt, die Garbage Collection zu implementieren, sind die beiden oben genannten Punkte für alle Garbage Collection-Implementierungen gleich. Die eigentliche Herausforderung der Garbage Collection besteht darin, festzustellen, ob ein Objekt aktiv ist, und wie man Speicher zurückgewinnen kann, ohne die Anwendung so stark wie möglich zu beeinträchtigen. Daher verfolgt der Garbage Collector die folgenden zwei Ziele:
1. Geben Sie nicht referenzierten Speicher schnell frei, um den Speicherzuweisungsbedarf der Anwendung zu decken und einen Speicherüberlauf zu vermeiden.
2. Minimieren Sie die Auswirkungen auf die Leistung laufender Anwendungen (Latenz und Durchsatz) bei der Speicherrückgewinnung.
Zwei Arten der Müllsammlung
Im ersten Artikel dieser Serie habe ich zwei Methoden der Garbage Collection vorgestellt, nämlich die Referenzzählung und die Tracking-Collection. Als Nächstes untersuchen wir diese beiden Ansätze weiter und stellen einige in Produktionsumgebungen verwendete Trace-Erfassungsalgorithmen vor.
Referenzzählsammler
Der Referenzzählkollektor zeichnet die Anzahl der Referenzen auf, die auf jedes Java-Objekt verweisen. Sobald die Anzahl der Referenzen, die auf ein Objekt verweisen, 0 erreicht, kann das Objekt sofort recycelt werden. Diese Unmittelbarkeit ist der Hauptvorteil eines referenzgezählten Kollektors, und es entsteht fast kein Overhead bei der Verwaltung von Speicher, auf den keine Referenz verweist, aber die Verfolgung der neuesten Referenzanzahl für jedes Objekt ist teuer.
Die Hauptschwierigkeit des Referenzzählsammlers besteht darin, die Genauigkeit der Referenzzählung sicherzustellen. Eine weitere bekannte Schwierigkeit ist der Umgang mit Zirkelverweisen. Wenn zwei Objekte aufeinander verweisen und nicht von anderen lebenden Objekten referenziert werden, wird der Speicher der beiden Objekte niemals zurückgefordert, da die Anzahl der Referenzen, die auf keines der Objekte verweisen, 0 ist. Das Speicherrecycling von zirkulären Referenzstrukturen erfordert eine umfassende Analyse (Anmerkung des Übersetzers: Globale Analyse auf dem Java-Heap), was die Komplexität des Algorithmus erhöht und somit zusätzlichen Overhead für die Anwendung mit sich bringt.
Spurensammler
Der Tracing-Kollektor basiert auf der Annahme, dass alle lebenden Objekte durch Iteration von Referenzen (Referenzen und Referenzen von Referenzen) auf einen bekannten anfänglichen Satz lebender Objekte gefunden werden können. Der anfängliche Satz aktiver Objekte (auch Wurzelobjekte genannt) kann durch die Analyse von Registern, globalen Objekten und Stapelrahmen bestimmt werden. Nach der Bestimmung des anfänglichen Satzes von Objekten verfolgt der Tracking-Kollektor die Referenzbeziehungen dieser Objekte und markiert die Objekte, auf die durch die Referenzen verwiesen wird, der Reihe nach als aktive Objekte, sodass der Satz bekannter aktiver Objekte weiter wächst. Dieser Prozess wird fortgesetzt, bis alle referenzierten Objekte als Live-Objekte markiert sind und der Speicher der nicht markierten Objekte zurückgefordert wird.
Der Tracking-Kollektor unterscheidet sich vom Referenz-Zähl-Kollektor hauptsächlich dadurch, dass er zirkuläre Referenzstrukturen verarbeiten kann. Die meisten Nachverfolgungssammler entdecken während der Markierungsphase nicht referenzierte Objekte in kreisförmigen Referenzstrukturen.
Der Tracing-Kollektor ist die am häufigsten verwendete Speicherverwaltungsmethode in dynamischen Sprachen und wird derzeit auch in Java verwendet. Er wird seit vielen Jahren auch in Produktionsumgebungen verifiziert. Im Folgenden werde ich den Trace-Kollektor vorstellen, beginnend mit einigen Algorithmen zur Implementierung der Trace-Sammlung.
Algorithmus zur Spurensammlung
Kopierende Garbage Collectors und Mark-Sweep Garbage Collectors sind nichts Neues, aber sie sind auch heute noch die beiden gängigsten Algorithmen zur Implementierung von Tracking-Sammlungen.
Kopieren des Garbage Collectors
Der herkömmliche Kopier-Garbage Collector verwendet zwei Adressräume im Heap (dh From Space und To Space). Wenn die Garbage Collection durchgeführt wird, werden alle aktiven Objekte im From Space in den To Space kopiert werden entfernt (Anmerkung des Übersetzers: Nach dem Kopieren in den To-Bereich oder die alte Generation) kann der gesamte From-Bereich recycelt werden. Wenn der Speicherplatz erneut zugewiesen wird, wird zuerst der To-Bereich verwendet (Anmerkung des Übersetzers: Das heißt, der To-Bereich der vorherigen Runde wird als neue Weltraumrunde verwendet.
In der frühen Implementierung dieses Algorithmus haben der From-Bereich und der To-Bereich kontinuierlich ihre Positionen geändert. Das heißt, wenn der To-Bereich voll ist und die Speicherbereinigung ausgelöst wird, wird der To-Bereich zum From-Bereich, wie in Abbildung 1 dargestellt .
Abbildung 1 Herkömmliche Garbage-Collection-Sequenz für Kopien
Der neueste Kopieralgorithmus ermöglicht die Nutzung jedes Adressraums im Heap als To-Space und From-Space. Auf diese Weise müssen sie ihre Positionen nicht untereinander tauschen, sondern wechseln einfach logisch ihre Positionen.
Der Vorteil des Kopiersammlers besteht darin, dass die kopierten Objekte im Zielraum kompakt angeordnet sind und keinerlei Fragmentierung auftritt. Fragmentierung ist ein häufiges Problem, mit dem andere Garbage Collectors konfrontiert sind, und es ist auch das Hauptproblem, auf das ich später noch eingehen werde.
Nachteile des Kopiersammlers
Im Allgemeinen ist der Kopierkollektor „Stop-the-World“, was bedeutet, dass die Anwendung nicht ausgeführt werden kann, solange die Speicherbereinigung läuft. Bei dieser Implementierung ist die Auswirkung auf die Anwendungsleistung umso größer, je mehr Dinge Sie kopieren müssen. Dies ist ein Nachteil für Anwendungen, die reaktionszeitempfindlich sind. Wenn Sie den Kopierkollektor verwenden, müssen Sie auch das schlimmste Szenario berücksichtigen (das heißt, alle Objekte im Von-Bereich sind aktive Objekte. Zu diesem Zeitpunkt müssen Sie einen ausreichend großen Bereich zum Verschieben dieser aktiven Objekte vorbereiten). Der Speicherplatz muss groß genug sein, um alle Objekte im From-Bereich zu installieren. Aufgrund dieser Einschränkung ist die Speicherauslastung des Kopieralgorithmus etwas unzureichend (Anmerkung des Übersetzers: Im schlimmsten Fall muss der To-Speicherplatz die gleiche Größe wie der From-Speicherplatz haben, sodass die Auslastung nur 50 % beträgt).
Markierungsfreier Sammler
Die meisten kommerziellen JVMs, die in Produktionsumgebungen von Unternehmen eingesetzt werden, verwenden einen Mark-Sweep-(oder Mark-)Collector, da dieser die Auswirkungen eines Garbage Collectors auf die Anwendungsleistung nicht reproduziert. Zu den bekanntesten Markensammlern zählen CMS, G1, GenPar und DeterministicGC.
Der Mark-Sweep-Kollektor verfolgt Objektreferenzen und markiert jedes gefundene Objekt mithilfe eines Flag-Bits als aktiv. Dieses Flag entspricht normalerweise einer Adresse oder einer Gruppe von Adressen auf dem Heap. Beispiel: Das aktive Bit kann ein Bit im Objektheader (Anmerkung des Übersetzers: Bit) oder ein Bitvektor oder eine Bitmap sein.
Nachdem die Markierung abgeschlossen ist, wird die Bereinigungsphase eingeleitet. In der Bereinigungsphase wird normalerweise der Heap erneut durchsucht (nicht nur als Live markierte Objekte, sondern der gesamte Heap), um nicht markierte zusammenhängende Speicheradressräume zu finden (nicht markierter Speicher ist frei und recycelbar), und dann organisiert der Collector sie in freie Listen. Der Garbage Collector kann über mehrere freie Listen verfügen (normalerweise unterteilt nach der Größe des Speicherblocks). Einige JVM-Sammler (z. B. JRockit Real Time) teilen die freie Liste sogar dynamisch auf der Grundlage von Anwendungsleistungsanalysen und Objektgrößenstatistiken auf.
Nach der Bereinigungsphase kann die Anwendung wieder Speicher zuweisen. Beim Zuweisen von Speicher für ein neues Objekt aus der freien Liste muss der neu zugewiesene Speicherblock zur Größe des neuen Objekts, zur durchschnittlichen Objektgröße des Threads oder zur TLAB-Größe der Anwendung passen. Das Finden von Speicherblöcken geeigneter Größe für neue Objekte hilft, den Speicher zu optimieren und die Fragmentierung zu reduzieren.
Markierung – Sammlermängel beseitigen
Die Ausführungszeit der Markierungsphase hängt von der Anzahl der aktiven Objekte im Heap ab, während die Ausführungszeit der Bereinigungsphase von der Größe des Heaps abhängt. Daher verfügt der Mark-Sweep-Algorithmus in Situationen, in denen die Heap-Einstellung groß ist und sich viele aktive Objekte im Heap befinden, über eine bestimmte Pausenzeit.
Für speicherintensive Anwendungen können Sie die Garbage-Collection-Parameter an verschiedene Anwendungsszenarien und -anforderungen anpassen. In vielen Fällen verschiebt diese Anpassung zumindest das von der Mark/Sweep-Phase ausgehende Risiko auf das SLA der Anwendung oder der Servicevereinbarung (SLA bezieht sich hier auf die Reaktionszeit, die die Anwendung erreichen muss). Die Optimierung ist jedoch nur für bestimmte Lasten und Speicherzuordnungsraten wirksam. Laständerungen oder Modifikationen an der Anwendung selbst erfordern eine Neuoptimierung.
Implementierung eines Mark-Sweep-Kollektors
Es gibt mindestens zwei kommerziell bewährte Methoden zur Implementierung der Mark-Sweep-Garbage Collection. Die eine ist die parallele Garbage Collection und die andere die gleichzeitige (oder meistens gleichzeitige) Garbage Collection.
Parallelkollektor
Parallele Sammlung bedeutet, dass Ressourcen von Garbage-Collection-Threads parallel verwendet werden. Bei den meisten kommerziellen Implementierungen der parallelen Sammlung handelt es sich um Stop-the-World-Collectors, bei denen alle Anwendungsthreads angehalten werden, bis eine Garbage Collection abgeschlossen ist. Da Garbage Collectors Ressourcen effizient nutzen können, erzielen sie in der Regel bessere Ergebnisse bei Durchsatz-Benchmarks SPECjbb. Wenn der Durchsatz für Ihre Anwendung von entscheidender Bedeutung ist, ist ein paralleler Garbage Collector eine gute Wahl.
Die Hauptkosten der parallelen Sammlung (insbesondere für Produktionsumgebungen) bestehen darin, dass Anwendungsthreads während der Garbage Collection nicht ordnungsgemäß funktionieren können, genau wie der Kopierkollektor. Daher wird die Verwendung paralleler Kollektoren erhebliche Auswirkungen auf Anwendungen haben, die empfindlich auf die Reaktionszeit reagieren. Insbesondere wenn sich im Heap-Raum viele komplexe aktive Objektstrukturen befinden, müssen viele Objektreferenzen verfolgt werden. (Bedenken Sie, dass die Zeit, die der Mark-Sweep-Kollektor benötigt, um Speicher zurückzugewinnen, von der Zeit abhängt, die zum Verfolgen der Sammlung von Live-Objekten benötigt wird, plus der Zeit, die zum Durchlaufen des gesamten Heaps benötigt wird.) Beim parallelen Ansatz wird die Anwendung für die Zeit angehalten gesamte Garbage Collection-Zeit.
gleichzeitiger Kollektor
Gleichzeitige Garbage Collectors eignen sich besser für Anwendungen, bei denen die Reaktionszeit empfindlich ist. Parallelität bedeutet, dass der Garbage-Collection-Thread und der Anwendungsthread gleichzeitig ausgeführt werden. Der Garbage-Collection-Thread besitzt nicht alle Ressourcen, daher muss er entscheiden, wann eine Garbage Collection gestartet werden soll, damit genügend Zeit bleibt, die aktive Objektsammlung zu verfolgen und den Speicher zurückzugewinnen, bevor der Anwendungsspeicher überläuft. Wenn die Garbage Collection nicht rechtzeitig abgeschlossen wird, gibt die Anwendung einen Speicherüberlauffehler aus. Andererseits soll die Garbage Collection nicht zu lange dauern, da sie die Ressourcen der Anwendung verbraucht und den Durchsatz beeinträchtigt. Das Aufrechterhalten dieses Gleichgewichts erfordert Geschick. Daher werden Heuristiken verwendet, um zu bestimmen, wann mit der Garbage Collection begonnen und wann Garbage Collection-Optimierungen ausgewählt werden sollten.
Eine weitere Schwierigkeit besteht darin, zu bestimmen, wann es sicher ist, einige Vorgänge auszuführen (Vorgänge, die einen vollständigen und genauen Heap-Snapshot erfordern), z. B. die Notwendigkeit zu wissen, wann die Markierungsphase abgeschlossen ist, damit in die Bereinigungsphase eingetreten werden kann. Für einen Stop-the-World-Parallelkollektor stellt dies kein Problem dar, da die Welt bereits angehalten ist (Anmerkung des Übersetzers: Der Anwendungsthread ist angehalten und der Garbage-Collection-Thread monopolisiert Ressourcen). Bei gleichzeitigen Sammlern ist es jedoch möglicherweise nicht sicher, sofort von der Markierungsphase in die Reinigungsphase zu wechseln. Wenn ein Anwendungsthread einen Teil des Speichers ändert, der vom Garbage Collector verfolgt und markiert wurde, können neue, nicht markierte Referenzen generiert werden. Bei einigen gleichzeitigen Sammlungsimplementierungen kann dies dazu führen, dass die Anwendung längere Zeit in einer Schleife wiederholter Anmerkungen stecken bleibt, ohne dass sie freien Speicher erhalten kann, wenn die Anwendung diesen Speicher benötigt.
Aus der bisherigen Diskussion wissen wir, dass es viele Garbage Collectors und Garbage Collection-Algorithmen gibt, die jeweils für bestimmte Anwendungstypen und unterschiedliche Lasten geeignet sind. Nicht nur unterschiedliche Algorithmen, sondern auch unterschiedliche Algorithmusimplementierungen. Daher ist es am besten, die Anforderungen der Anwendung und ihre eigenen Eigenschaften zu verstehen, bevor Sie einen Garbage Collector angeben. Als Nächstes stellen wir einige Fallstricke des Java-Plattform-Speichermodells vor. Die Fallstricke beziehen sich auf einige Annahmen, die Java-Programmierer in einer sich dynamisch ändernden Produktionsumgebung machen, die die Anwendungsleistung verschlechtern.
Warum Tuning die Garbage Collection nicht ersetzen kann
Die meisten Java-Programmierer wissen, dass es viele Möglichkeiten gibt, Java-Programme zu optimieren. Mehrere optionale JVM-, Garbage Collector- und Leistungsoptimierungsparameter ermöglichen es Entwicklern, viel Zeit in endlose Leistungsoptimierungen zu investieren. Dies hat einige Leute zu dem Schluss geführt, dass die Garbage Collection schlecht ist und dass eine Optimierung, die dafür sorgt, dass die Garbage Collection seltener erfolgt oder kürzer dauert, ein guter Workaround ist, aber das ist riskant.
Erwägen Sie eine Optimierung für eine bestimmte Anwendung (z. B. Speicherzuweisungsrate, Objektgröße, Antwortzeit) basierend auf der Speicherzuweisungsrate (Anmerkung des Übersetzers: oder anderen Parametern) basierend auf dem aktuellen Testdatenvolumen. Dies kann letztendlich zu den folgenden zwei Ergebnissen führen:
1. Ein Anwendungsfall, der die Tests bestanden hat, schlägt in der Produktion fehl.
2. Änderungen im Datenvolumen oder Änderungen in Anwendungen erfordern eine Neuabstimmung.
Die Optimierung erfolgt iterativ, und insbesondere gleichzeitige Garbage Collectors erfordern möglicherweise umfangreiche Optimierungen (insbesondere in einer Produktionsumgebung). Heuristiken sind erforderlich, um die Anforderungen der Anwendung zu erfüllen. Um dem Worst-Case-Szenario gerecht zu werden, kann das Ergebnis des Tunings eine sehr starre Konfiguration sein, was auch zu einer hohen Ressourcenverschwendung führt. Dieser Tuning-Ansatz ist eine weltfremde Aufgabe. Tatsächlich gilt: Je mehr Sie den Garbage Collector für eine bestimmte Last optimieren, desto weiter entfernen Sie sich von der dynamischen Natur der Java-Laufzeitumgebung. Denn wie viele Anwendungen haben eine stabile Auslastung und wie zuverlässig können Sie mit der Auslastung rechnen?
Wenn Sie sich also nicht auf die Optimierung konzentrieren, was können Sie dann tun, um Fehler aufgrund von unzureichendem Arbeitsspeicher zu verhindern und die Antwortzeiten zu verbessern? Zunächst müssen die Hauptfaktoren ermittelt werden, die die Leistung von Java-Anwendungen beeinflussen.
Zersplitterung
Der Faktor, der die Leistung von Java-Anwendungen beeinflusst, ist nicht der Garbage Collector, sondern die Fragmentierung und die Art und Weise, wie der Garbage Collector mit der Fragmentierung umgeht. Die sogenannte Fragmentierung ist ein Zustand, in dem im Heap-Speicher freier Speicherplatz vorhanden ist, aber nicht genügend zusammenhängender Speicherplatz vorhanden ist, um Speicher für neue Objekte zuzuweisen. Wie im ersten Artikel erwähnt, handelt es sich bei der Speicherfragmentierung entweder um eine TLAB des im Heap verbleibenden Speicherplatzes oder um den Speicherplatz, der von kleinen Objekten belegt wird, die zwischen langlebigen Objekten freigegeben werden.
Mit der Zeit und während die Anwendung ausgeführt wird, breitet sich diese Fragmentierung im gesamten Heap aus. In manchen Fällen kann die Verwendung statisch abgestimmter Parameter schlechter sein, da sie den dynamischen Anforderungen der Anwendung nicht gerecht werden. Anwendungen können diesen fragmentierten Raum nicht effizient nutzen. Wenn Sie nichts unternehmen, kommt es zu aufeinanderfolgenden Garbage Collections, bei denen der Garbage Collector versucht, Speicher für die Zuweisung an neue Objekte freizugeben. Im schlimmsten Fall können selbst aufeinanderfolgende Garbage Collections nicht mehr Speicher freigeben (zu starke Fragmentierung), und dann muss die JVM einen Speicherüberlauffehler auslösen. Sie können die Fragmentierung beheben, indem Sie die Anwendung neu starten, sodass der Java-Heap über zusammenhängenden Speicherplatz für die Zuweisung neuer Objekte verfügt. Ein Neustart des Programms führt zu Ausfallzeiten und nach einer Weile wird der Java-Heap wieder voller Fragmente sein, was einen weiteren Neustart erzwingt.
Fehler wegen unzureichendem Arbeitsspeicher, die den Prozess zum Stillstand bringen, und Protokolle, aus denen hervorgeht, dass der Garbage Collector überlastet ist, deuten darauf hin, dass die Garbage Collection versucht, Speicher freizugeben, und dass der Heap stark fragmentiert ist. Einige Programmierer werden versuchen, das Fragmentierungsproblem zu lösen, indem sie den Garbage Collector erneut optimieren. Aber ich denke, wir sollten innovativere Wege finden, dieses Problem zu lösen. Die folgenden Abschnitte konzentrieren sich auf zwei Lösungen zur Fragmentierung: generationsübergreifende Garbage Collection und Komprimierung.
Generationsübergreifende Müllabfuhr
Möglicherweise haben Sie die Theorie gehört, dass die meisten Objekte in einer Produktionsumgebung nur von kurzer Dauer sind. Generational Garbage Collection ist eine aus dieser Theorie abgeleitete Garbage Collection-Strategie. Bei der Generationen-Garbage-Collection teilen wir den Heap in verschiedene Räume (oder Generationen) auf, und jeder Raum speichert Objekte unterschiedlichen Alters. Das sogenannte Alter eines Objekts ist die Anzahl der Garbage-Collection-Zyklen, die das Objekt überstanden hat (d. h. wie alt das Objekt ist).
Wenn in der neuen Generation kein Speicherplatz mehr vorhanden ist, werden die aktiven Objekte der neuen Generation in die alte Generation verschoben (normalerweise gibt es nur zwei Generationen). Anmerkung des Übersetzers: Nur Objekte, die ein bestimmtes Alter erreichen, werden in die alte Generation verschoben Die Garbage Collection der alten Generation verwendet häufig einen Einweg-Kopie-Collector. Natürlich können für die neue Generation und die alte Generation unterschiedliche Garbage-Collectors implementiert werden. Wenn Sie einen Parallelkollektor oder einen Kopierkollektor verwenden, ist Ihr junger Sammler ein „Stop-the-World“-Kollektor (siehe vorherige Erklärung).
Die alte Generation wird Objekten zugeordnet, die aus der neuen Generation verschoben wurden. Auf diese Objekte wurde entweder schon seit langem verwiesen oder sie werden von einer Sammlung von Objekten in der neuen Generation referenziert. Gelegentlich werden große Objekte direkt der alten Generation zugeordnet, da die Kosten für den Umzug großer Objekte relativ hoch sind.
Generationsübergreifende Garbage-Collection-Technologie
Bei der generationsübergreifenden Garbage Collection wird die Garbage Collection in der alten Generation seltener und in der neuen Generation häufiger ausgeführt, und wir hoffen auch, dass der Garbage Collection-Zyklus in der neuen Generation kürzer wird. In seltenen Fällen kann es vorkommen, dass die junge Generation häufiger gesammelt wird als die alte Generation. Dies kann passieren, wenn Sie die junge Generation zu groß machen und die meisten Objekte in Ihrer Anwendung lange leben. Wenn in diesem Fall die alte Generation zu klein eingestellt ist, um alle langlebigen Objekte aufzunehmen, wird es auch für die Speicherbereinigung der alten Generation schwierig sein, Platz für die verschobenen Objekte freizugeben. Im Allgemeinen kann die generationsübergreifende Garbage Collection jedoch dazu führen, dass Anwendungen eine bessere Leistung erzielen.
Ein weiterer Vorteil der Spaltung der neuen Generation besteht darin, dass sie das Fragmentierungsproblem bis zu einem gewissen Grad löst oder das Worst-Case-Szenario hinauszögert. Diese kleinen Objekte mit kurzen Überlebenszeiten haben möglicherweise Fragmentierungsprobleme verursacht, werden jedoch alle in der Garbage Collection der neuen Generation bereinigt. Da langlebigen Objekten beim Verschieben in die alte Generation kompakterer Platz zugewiesen wird, ist die alte Generation auch kompakter. Mit der Zeit (wenn Ihre Anwendung lange genug läuft) wird auch die alte Generation fragmentiert, sodass eine oder mehrere vollständige Garbage Collections ausgeführt werden müssen, und die JVM kann außerdem Fehler wegen unzureichendem Arbeitsspeicher auslösen. Doch die Entwicklung einer neuen Generation verschiebt den Worst-Case-Szenario, der für viele Anwendungen ausreicht. Bei den meisten Anwendungen werden dadurch die Häufigkeit der Speicherbereinigung „Stop-the-World“ und die Wahrscheinlichkeit von Fehlern wegen unzureichendem Arbeitsspeicher verringert.
Optimieren Sie die generationsübergreifende Speicherbereinigung
Wie bereits erwähnt, bringt die Verwendung der generationsübergreifenden Garbage Collection wiederholte Optimierungsarbeiten mit sich, wie z. B. die Anpassung der Größe der jungen Generation, der Beförderungsrate usw. Ich kann den Kompromiss für eine bestimmte Anwendungslaufzeit nicht betonen: Die Auswahl einer festen Größe optimiert die Anwendung, verringert aber auch die Fähigkeit des Garbage Collectors, mit dynamischen Änderungen umzugehen, die unvermeidlich sind.
Das erste Prinzip für die neue Generation besteht darin, es so weit wie möglich zu erhöhen und gleichzeitig die Verzögerungszeit während der Stop-the-World-Garbage Collection sicherzustellen und gleichzeitig genügend Platz im Heap für langfristig überlebende Objekte zu reservieren. Hier sind einige zusätzliche Faktoren, die Sie bei der Optimierung eines Generations-Garbage Collectors berücksichtigen sollten:
1. Die meisten der neuen Generation sind Stop-the-World-Garbage Collectors. Je größer die Einstellung der neuen Generation, desto länger ist die entsprechende Pausenzeit. Überlegen Sie daher bei Anwendungen, die stark von den Pausenzeiten der Garbage Collection betroffen sind, sorgfältig, wie groß die junge Generation ist.
2. Für verschiedene Generationen können unterschiedliche Garbage-Collection-Algorithmen verwendet werden. Beispielsweise wird in der jungen Generation die parallele Garbage Collection und in der alten Generation die gleichzeitige Garbage Collection verwendet.
3. Wenn festgestellt wird, dass die häufige Förderung (Anmerkung des Übersetzers: Wechsel von der neuen zur alten Generation) fehlschlägt, bedeutet dies, dass in der alten Generation zu viele Fragmente vorhanden sind, was bedeutet, dass in der alten Generation nicht genügend Platz vorhanden ist zum Speichern von Objekten, die von der neuen Generation verschoben wurden. An diesem Punkt können Sie die Heraufstufungsrate anpassen (d. h. das Heraufstufungsalter anpassen) oder sicherstellen, dass der Garbage-Collection-Algorithmus der alten Generation die Komprimierung durchführt (siehe nächster Absatz) und die Komprimierung an die Auslastung der Anwendung anpassen . Es ist auch möglich, die Heap-Größe und die Größe jeder Generation zu erhöhen, dies führt jedoch zu einer weiteren Verlängerung der Pausenzeit auf der alten Generation. Seien Sie sich bewusst, dass Fragmentierung unvermeidlich ist.
4. Die Generationen-Garbage-Collection ist für solche Anwendungen am besten geeignet. Sie haben viele kleine Objekte mit kurzer Überlebenszeit. Viele Objekte werden in der ersten Runde des Garbage-Collection-Zyklus recycelt. Bei solchen Anwendungen kann die generationsübergreifende Speicherbereinigung die Fragmentierung effektiv reduzieren und die Auswirkungen der Fragmentierung verzögern.
Kompression
Obwohl die generationsübergreifende Garbage Collection das Auftreten von Fragmentierungs- und Speicherfehlern verzögert, ist die Komprimierung die einzige wirkliche Lösung für das Fragmentierungsproblem. Komprimierung ist eine Garbage-Collection-Strategie, die zusammenhängende Speicherblöcke durch Verschieben von Objekten freigibt und so genügend Platz für die Erstellung neuer Objekte freigibt.
Das Verschieben von Objekten und das Aktualisieren von Objektreferenzen sind Stop-the-World-Vorgänge, die einen gewissen Verbrauch mit sich bringen (mit einer Ausnahme, die im nächsten Artikel dieser Serie besprochen wird). Je mehr Objekte überleben, desto länger ist die durch die Verdichtung verursachte Pausenzeit. In Situationen mit wenig verbleibendem Speicherplatz und starker Fragmentierung (normalerweise, weil das Programm schon seit langer Zeit ausgeführt wird) kann es beim Komprimieren von Bereichen mit vielen lebenden Objekten zu einer Pause von einigen Sekunden kommen Der gesamte Heap kann sogar mehrere zehn Sekunden dauern.
Die Pausenzeit für die Komprimierung hängt von der Speichermenge ab, die verschoben werden muss, und von der Anzahl der Referenzen, die aktualisiert werden müssen. Die statistische Analyse zeigt, dass je größer der Heap ist, desto mehr Live-Objekte müssen verschoben und Referenzen aktualisiert werden. Die Pausenzeit beträgt etwa 1 Sekunde pro 1 GB bis 2 GB an verschobenen Live-Objekten, und bei einem 4 GB großen Heap ist es wahrscheinlich, dass 25 % Live-Objekte vorhanden sind, sodass es gelegentlich zu Pausen von etwa 1 Sekunde kommt.
Komprimierungs- und Anwendungsspeicherwand
Die Anwendungsspeichermauer bezieht sich auf die Heap-Größe, die vor einer durch die Speicherbereinigung verursachten Pause (z. B. Komprimierung) festgelegt werden kann. Abhängig vom System und der Anwendung reichen die Speicherwände der meisten Java-Anwendungen von 4 GB bis 20 GB. Aus diesem Grund werden die meisten Unternehmensanwendungen auf mehreren kleineren JVMs bereitgestellt und nicht auf einigen größeren JVMs. Betrachten wir Folgendes: Wie viele moderne Java-Anwendungsdesigns und -bereitstellungen für Unternehmen werden durch die Komprimierungsbeschränkungen der JVM definiert? Um die Pausenzeit der Defragmentierung des Heaps zu umgehen, haben wir uns in diesem Fall für eine Bereitstellung mit mehreren Instanzen entschieden, deren Verwaltung teurer war. Dies ist angesichts der großen Speicherkapazitäten der heutigen Hardware und des Bedarfs an mehr Speicher für Java-Anwendungen der Enterprise-Klasse etwas seltsam. Warum für jede Instanz nur wenige GB Speicher festgelegt sind. Durch die gleichzeitige Komprimierung wird die Speichermauer durchbrochen, was das Thema meines nächsten Artikels ist.
Zusammenfassen
Bei diesem Artikel handelt es sich um einen Einführungsartikel zur Garbage Collection, der Ihnen helfen soll, die Konzepte und Mechanismen der Garbage Collection zu verstehen und Sie hoffentlich dazu motivieren soll, weitere verwandte Artikel zu lesen. Viele der hier besprochenen Dinge gibt es schon seit langem und einige neue Konzepte werden im nächsten Artikel vorgestellt. Beispielsweise wird die gleichzeitige Komprimierung derzeit von der Zing-JVM von Azul implementiert. Es handelt sich um eine aufstrebende Garbage-Collection-Technologie, die sogar versucht, das Java-Speichermodell neu zu definieren, insbesondere da Speicher und Verarbeitungsleistung heutzutage immer besser werden.
Hier sind einige wichtige Punkte zur Garbage Collection, die ich zusammengefasst habe:
1. Unterschiedliche Garbage-Collection-Algorithmen und -Implementierungen passen sich an unterschiedliche Anwendungsanforderungen an. Der Tracking-Garbage Collector ist der am häufigsten verwendete Garbage Collector in kommerziellen Java Virtual Machines.
2. Die parallele Garbage Collection nutzt bei der Garbage Collection alle Ressourcen parallel. Es handelt sich normalerweise um einen Garbage Collector, der die Welt stoppt, und hat daher einen höheren Durchsatz. Die Arbeitsthreads der Anwendung müssen jedoch warten, bis der Garbage Collection-Thread abgeschlossen ist, was einen gewissen Einfluss auf die Antwortzeit der Anwendung hat.
3. Gleichzeitige Garbage Collection: Während die Sammlung durchgeführt wird, läuft der Anwendungs-Worker-Thread noch. Ein gleichzeitiger Garbage Collector muss die Garbage Collection abschließen, bevor die Anwendung den Speicher benötigt.
4. Die generationsübergreifende Garbage Collection hilft, die Fragmentierung zu verzögern, kann sie jedoch nicht beseitigen. Die generationsübergreifende Garbage Collection unterteilt den Heap in zwei Bereiche, einen für neue Objekte und einen für alte Objekte. Die generationsübergreifende Garbage Collection eignet sich für Anwendungen mit vielen kleinen Objekten, die eine kurze Lebensdauer haben.
5. Komprimierung ist die einzige Möglichkeit, die Fragmentierung zu lösen. Die meisten Garbage Collectors führen die Komprimierung nach dem Stop-the-World-Prinzip durch. Je länger das Programm läuft, desto komplexer sind die Objektreferenzen und desto ungleichmäßiger sind die Objektgrößen verteilt, was zu längeren Komprimierungszeiten führt. Die Größe des Heaps wirkt sich auch auf die Komprimierungszeit aus, da möglicherweise mehr Live-Objekte und Referenzen aktualisiert werden müssen.
6. Die Optimierung trägt dazu bei, Speicherüberlauffehler zu verzögern. Das Ergebnis einer Überabstimmung ist jedoch eine starre Konfiguration. Bevor Sie mit der Optimierung durch einen Versuch-und-Irrtum-Ansatz beginnen, stellen Sie sicher, dass Sie die Belastung Ihrer Produktionsumgebung, die Objekttypen Ihrer Anwendung und die Merkmale Ihrer Objektreferenzen verstehen. Zu starre Konfigurationen sind möglicherweise nicht in der Lage, dynamische Belastungen zu bewältigen. Stellen Sie daher sicher, dass Sie die Konsequenzen verstehen, wenn Sie nicht dynamische Werte festlegen.
Der nächste Artikel in dieser Reihe ist: Eine ausführliche Diskussion des C4-Garbage-Collection-Algorithmus (Concurrent Continuously Compacting Collector), also bleiben Sie dran!
(Volltext endet)