Java Garbage Collection Boot Camp (Entwurf)

UPDATE: Ich habe Version 1.0 fertiggestellt: http://www.lucidimagination.com/blog/2011/03/27/garbage-collection-bootcamp-1-0/

Ich arbeite an einem Artikel über Garbage Collection – ich dachte mir, ich teile einen frühen Rohentwurf mit Ihnen:

Es ist zwar nicht oft der Fall, aber manchmal wird die Garbage Collection bei der Arbeit mit einer großen und ausgelasteten Solr/Lucene-Installation zum Engpass. Dieser Leitfaden soll Ihnen helfen, diesen Engpass zu beseitigen, falls er auftreten sollte.

Bei der Garbage Collection in Java geht es darum, den von nicht mehr verwendeten Objekten belegten Speicher freizugeben. In C oder C++ wären Sie selbst dafür verantwortlich, diesen Speicher freizugeben, aber in Java wird diese Aufgabe dem Garbage Collector überlassen, so dass sich der Programmierer auf andere Aufgaben konzentrieren und die Speicherverwaltung der JVM überlassen kann.

Der einfachste Garbage Collection-Algorithmus beginnt mit den Root-Objekten (d.h. Objekten auf dem Thread-Stack, statischen Objekten usw.), die live sind (live bedeutet, dass sie gerade verwendet werden), und iteriert dann über jedes erreichbare Objekt nach unten. Jedes Objekt, das auf diese Weise nicht erreicht werden kann, ist Müll und kann eingesammelt werden. Die Anwendung wird angehalten, während dieser Prozess abläuft. Dies wird als Markieren und Durchsuchen bezeichnet – zuerst markieren Sie die Objekte, die aktiv sind, dann durchsuchen Sie die, die nicht aktiv sind. Die dafür benötigte Zeit ist natürlich proportional zur Anzahl der aktiven Objekte (die in modernen Java-Anwendungen recht hoch sein kann), so dass effizientere Sammelverfahren entwickelt wurden.

Ein solches Schema ergibt sich aus der natürlichen Tatsache, dass Sie Objekte nach ihrer Lebensdauer aufteilen können. Die meisten Anwendungen erzeugen eine Menge sehr kurzlebiger Objekte und weniger Objekte, die lange Zeit bestehen (ich habe Schätzungen gesehen, dass bei einer durchschnittlichen Anwendung 85-98% der zugewiesenen Objekte kurzlebig sind). Diese Tatsache können Sie sich bei der Erstellung von Sammlungen zunutze machen. Der Java-Heap ist in der Regel in mehrere Bereiche unterteilt (in der Regel ist er bei allen Implementierungen gleich, aber es gibt auch die eine oder andere Ausnahme). Die wichtigsten Bereiche sind die junge Generation, die dauerhafte Generation (auch alte Generation genannt) und die permanente Generation. Die junge Generation ist dann weiter unterteilt in den Eden-Raum und zwei Überlebensräume. Die permanente Generation ist in der Regel für Objekte gedacht, die ein Leben lang in der Anwendung verbleiben (internierte Strings, Klassenobjekte usw.) und spielt normalerweise keine große Rolle bei der Garbage Collection (obwohl die permanente Generation bei Bedarf mit -XX:+CMSPermGenSweepingEnabled gesammelt werden kann).

gc Räume

Wenn Objekte zum ersten Mal erstellt werden, werden sie dem Eden-Bereich zugewiesen. Wenn der Eden-Space voll ist, werden die noch lebenden Objekte in einen der Survivor-Spaces kopiert (oder, wenn sie nicht mehr hineinpassen, in den Tenured-Space). Ein Überlebensraum bleibt immer leer, und bei jeder Sammlung der jungen Generation (einer kleineren Sammlung) werden die lebenden Objekte aus dem Edenraum und dem nicht leeren Überlebensraum in den leeren Überlebensraum kopiert. Auf diese Weise bleibt ein neu geleerter Überlebensraum für die nächste Runde übrig, da alle noch lebenden Objekte aus dem ehemals vollen Überlebensraum in den geleerten Raum kopiert werden.

Wie Sie sehen, müssen Sie jetzt nicht mehr jedes Objekt bei jeder Sammlung durchgehen, sondern können die jungen Generationen häufiger und die langlebigen Generationen (Objekte mit langer Lebensdauer) viel seltener sammeln. Sie können Ihre Sammlung auch für die Eigenschaften des Raums optimieren – d.h. in der Regel werden fast alle Objekte im jungen Raum Müll sein. Im Allgemeinen muss ein Objekt ein paar kleinere Sammlungen überleben, um in den Tenured Space zu gelangen (zuerst in den Survivor Space und dann in den Tenured Space).

Tuning für die Garbage Collection bedeutet, dass Sie die Größe dieser verschiedenen Bereiche sowie die Algorithmen, die zum Sammeln dieser Bereiche verwendet werden, anpassen. Sie können dies mit verschiedenen JVM-Befehlszeilenoptionen tun.

Die Menge an RAM, die für die verschiedenen Bereiche verfügbar ist, hängt von der Größe des Heaps ab, den die JVM zugewiesen hat. Die Standardeinstellungen werden auf der Grundlage der erkannten Hardware gewählt, aber Sie können in der Regel besser abschneiden, wenn Sie selbst ein gutes Xms, Xmx angeben. Auf einem Server-Rechner kann es sinnvoll sein, diese beiden Einstellungen zusammenzulegen, damit die JVM keine Zeit mit der Größenanpassung verschwendet.

Dimensionierung der einzelnen Räume:

In der Regel sollten Sie der jungen Generation viel Speicherplatz zugestehen – vor allem, wenn Sie mehrere Prozessoren haben -, da die Zuweisung parallelisiert werden kann und jeder Thread seinen eigenen privaten Teil des Eden-Speicherplatzes erhält, mit dem er arbeiten kann. Im Allgemeinen sollten Sie der jungen Generation jedoch weniger als die Hälfte des Speicherplatzes der etablierten Generation zugestehen – vor allem, wenn Sie den Serialized Collector verwenden. Etwa 33% ist normalerweise ein guter Wert für den Anfang. Die optimale Größe ist von Anwendung zu Anwendung unterschiedlich und hängt von der Verteilung der jungen und langlebigen Objekte ab. Sie wollen nicht, dass der Young Space so klein ist, dass sich viele kurzlebige Objekte im Tenured Space stapeln. Sie wollen auch nicht, dass er so groß ist, dass der Tenured Space nicht mehr genug Platz hat und/oder die Sammlungen der jungen Generation zu lange brauchen, um abgeschlossen zu werden.

Neben der Dimensionierung des gesamten Haufens kann die Dimensionierung der neuen Generation (ein anderer Name für die junge Generation) das wichtigste Element für eine gute Leistung sein.

-XX:NewSize – Größe der jungen Generation beim Start der JVM – wird automatisch berechnet, wenn Sie -XX:NewRatio angeben

-XX:MaxNewSize – Die größte Größe, auf die die junge Generation anwachsen kann (unbegrenzt, wenn nicht angegeben)

-Xmn – setzt die neue Generation auf eine feste Größe – dies wird normalerweise nicht empfohlen, es sei denn, Sie legen auch die anderen Speichergrößen fest.

-XX:NewRatio=n (dynamische Größe) setzt die Größe der neuen Generation in ein Verhältnis zur Größe der alten Generation.

Sie können auch die Größe der überlebenden Leerzeichen einstellen – in der Praxis ist dies jedoch meist nicht sehr hilfreich.

-XX:Überlebendenquote=6

Die beste Größe wird in der Regel durch Spielen mit den Parametern und anschließendes Testen der Leistung Ihrer Anwendung ermittelt. Es gibt ein paar hilfreiche Tools, die Ihnen einen Einblick in den Garbage Collection-Prozess geben.

Sie können die folgenden Befehlszeilenoptionen verwenden, um Informationen über den Garbage Collection-Prozess zu generieren:

-verbose:gc – gibt Informationen über Heap und gc für jede Sammlung aus.

-XX:+DruckeGCDetails – druckt zusätzliche gc-Informationen

-XX:+PrintGCTimeStamps – Zeitstempel hinzufügen

Es gibt auch ein sehr cooles Tool namens VisualGC, mit dem Sie visuell beobachten können, wie sich Objekte zwischen Räumen bewegen. Es ist als eigenständige Anwendung oder als Plugin für Netbeans und VisualVM erhältlich.

Die Sammler:

Es gibt drei Haupt-Collectors, mit denen Sie sich beschäftigen sollten (vieles davon gilt für Java 1.4, aber im Allgemeinen ziele ich auf Java 1.5 und höher ab). Diese Collectors sind: der Serialized Collector, der Throughput Collector und der Concurrent Low Pause Collector. Es gibt noch einen weiteren Collector der jungen Generation, der mit dem Throughput Collector gepaart werden kann, den Parallel Young Generation Collector.

Es gibt auch einen älteren inkrementellen Collector (der nicht unterstützt wird und auch als Zug-Collector bezeichnet wird) und einen inkrementellen Sammelmodus für den gleichzeitigen Low-Pause-Collector (den ich anspreche und der im Allgemeinen verwendet wird, wenn nur eine oder zwei CPUs zur Verfügung stehen), aber ich überlasse es Ihnen, diese selbst zu erforschen, wenn Sie daran interessiert sind.

Der Sammler in Serie

-XX:+BenutzungSerialGC

Mit dem serialisierten Collector wird eine große Sammlung durchgeführt, wenn der Tenure-Speicher voll ist. Dies wird als „stop the world“-Abholung bezeichnet, da alle Anwendungs-Threads angehalten werden, während die Abholung stattfindet.

Dieser Collector eignet sich am besten für kleine Anwendungen, Anwendungen, die auf einem Rechner mit nur einer CPU laufen, oder Anwendungen, bei denen die Pausenzeiten keine Rolle spielen. Dieser Collector ist relativ effizient, da er nicht zwischen Threads kommunizieren muss, aber Sie müssen bereit sein, die Pausen, in denen die Welt angehalten wird, zu akzeptieren. Kleinere Sammlungen halten ebenfalls „die Welt an“, sind aber im Allgemeinen recht effizient und schnell.

Der Durchsatzkollektor (auch bekannt als Parallelkollektor)

-XX:+BenutzungParallelGC

Der Kollektor für den Durchsatz verwendet eine parallele Version des Kollektors für die junge Generation, während der Kollektor für die alte Generation weiterhin den seriellen Kollektor verwendet. Während also ein einzelner Thread weiterhin die Sammlungen für den Tenured Space durchführt, arbeiten mehrere Threads zusammen, um den Young Space zu sammeln.

In Java 1.5 Update 6 wurde eine Funktion namens parallele Verdichtung hinzugefügt – diese Funktion ermöglicht es dem Throughput Collector, auch große Sammlungen parallel durchzuführen. Sie können dies mit -XX:+UseParallelOldGC aktivieren. Dies sollte sich positiv auf die Skalierbarkeit auswirken, da Sie so den Engpass eines einzelnen Sammelthreads umgehen können.

Der Durchsatz-Collector sollte der Standard-Collector sein, der auf Rechnern der Serverklasse (in Java 1.5 und höher) gewählt wird. Wenn ich mich recht erinnere, wird ein 32-Bit-Windows-Rechner nie als Serverklasse erkannt – andere Betriebssysteme werden als Serverklasse eingestuft, je nachdem, wie viel RAM und CPUs ihnen zur Verfügung stehen. Sie können diese Standardeinstellungen jedoch jederzeit überschreiben.

Der Durchsatz ist in der Regel am nützlichsten, wenn Ihre Anwendung eine große Anzahl von Threads hat, die neue Objekte erstellen, und Sie mehr als einen Prozessor zur Verfügung haben (am besten jedoch mehr als zwei). Wenn Sie mehrere Threads haben, die Objekte zuweisen, wollen Sie in der Regel auch die Größe der jungen Generation erhöhen. Die Anzahl der Garbage Collector-Threads entspricht in der Regel der Anzahl der Prozessoren, die Sie haben, aber Sie können diese Anzahl mit -XX:ParallelGCThreads=n steuern. Manchmal möchten Sie die Anzahl der Threads verringern, da jeder Thread einen Teil der Tenured Generation für Promotionen reserviert. Dies kann zu einem Fragmentierungseffekt führen und die Größe der Tenured Generation effektiv verringern (dies ist im Allgemeinen nur ein Problem, wenn Ihre Anwendung Zugriff auf viele Prozessoren oder Kerne hat).

Der Durchsatzsammler unterstützt auch die so genannte Ergonomie. Im Rahmen dieser Unterstützung können Sie verschiedene gewünschte Verhaltensweisen für Ihre Anwendung angeben, und die JVM wird versuchen, verschiedene Einstellungen zu optimieren, um Ihre Ziele zu erreichen.

-XX:MaxGCPauseMillis=n ein Hinweis für den Durchsatzkollektor, dass eine maximale Pausenzeit von n Millisekunden erwünscht ist. In der Voreinstellung gibt es keinen Hinweis. Der Collector passt die Heap-Größe und andere Sammelparameter an, um das Ziel zu erreichen. Beachten Sie, dass der Durchsatz bei dem Versuch, dieses Ziel zu erreichen, beeinträchtigt werden kann. Es gibt auch keine Garantie, dass das Ziel erreicht wird.

Sie können mit -XX:GCTimeRatio auch ein Ziel festlegen, wie viel Zeit im Vergleich zur Ausführung Ihrer Anwendung für die Garbage Collection aufgewendet wird. Standardmäßig ist dies auf 1% eingestellt (beachten Sie, dass sich diese Standardwerte von Version zu Version ändern können).

Mit dem serialisierten Garbage Collector wird eine Generation eingesammelt, wenn sie voll ist (d.h. wenn keine weiteren Zuweisungen aus dieser Generation vorgenommen werden können). Dies gilt auch für den Durchsatzsammler.

Der Sammler für gleichzeitige niedrige Pausen

-XX:+BenutzeConcMarkSweepGC

Verwenden Sie den gleichzeitigen Low-Pause-Collector, wenn Sie es sich leisten können, die Prozessorressourcen mit dem Garbage Collector zu teilen, während die Anwendung läuft. Dies ist in der Regel gut für eine Anwendung mit vielen langlebigen Daten – was bedeutet, dass Sie einen großen, dauerhaften Generierungsbereich benötigen. Natürlich ist es auch hilfreich, mehrere Prozessoren zu haben. Dieser Kollektor hält die Anwendungsthreads während einer Sammlung immer noch zweimal an – einmal kurz zu Beginn (wenn er Objekte markiert, auf die von Root-Objekten aus direkt zugegriffen werden kann) und eine etwas längere Pause zur Mitte hin (wenn er nach den Objekten sucht, die er aufgrund der parallelen Markierung übersehen hat) – der Rest der Sammlung wird gleichzeitig mit einem der verfügbaren Prozessoren (oder einem Thread) durchgeführt. Wenn dieser Collector die Sammlung nicht abschließen kann, bevor er voll ist, werden alle Threads angehalten und eine vollständige Sammlung durchgeführt – das bedeutet wahrscheinlich, dass Sie die Parameter für die gleichzeitige Sammlung anpassen müssen.

Dieser Collector wird für die Tenured Generation verwendet und führt die Sammlung gleichzeitig mit der Ausführung der Anwendung durch. Dieser Kollektor kann auch mit einer parallelen Version des Kollektors für die junge Generation gekoppelt werden (-XX:+UseParNewGC).

Beachten Sie, dass -XX:+UseParallelGC (der Durchsatzkollektor) nicht mit -XX:+UseConcMarkSweepGC verwendet werden sollte. Die JVM wird beim Start fehlschlagen, wenn Sie dies mit den meisten modernen JVMs versuchen.

Der Sammler für gleichzeitige niedrige Pausen führt Statistiken, so dass er am besten abschätzen kann, wann er mit dem Sammeln beginnt (so dass er fertig ist, bevor der Speicherplatz voll ist). Er beginnt aber auch mit dem Sammeln, wenn der Speicherplatz einen bestimmten Prozentsatz des verfügbaren Speicherplatzes erreicht – Sie können dies manuell mit -XX:CMSInitiatingOccupancyFraction=n einstellen. Der Standardwert für diese Einstellung variiert von JVM zu JVM. Ich habe gelesen, dass der Standardwert für 1.5 68% beträgt, während der Standardwert für 1.6 92% beträgt. Sie können diesen Wert bei Bedarf herabsetzen, um sicherzustellen, dass die Sammlung früher beginnt. Dann ist es wahrscheinlicher, dass Sie die Sammlung beenden, bevor der Speicherplatz voll ist.

Der gleichzeitige Low-Pause-Collector kann auch in einem inkrementellen Modus verwendet werden, auf den ich hier nicht näher eingehen werde. In diesem Modus gibt der Low-Pause-Collector den Prozessor, der für die parallele Sammlung verwendet wird, gelegentlich an die Anwendung zurück und verringert so seine Auswirkungen auf die Anwendungsleistung.

Der parallele Sammler der jungen Generation

-XX:+VerwendungParNewGC

Dieser Kollektor ähnelt dem Durchsatzkollektor insofern, als er die junge Generation parallel sammelt. Das meiste, was für den Durchsatzkollektor gilt, gilt auch für diesen Kollektor. Allerdings wird eine andere Implementierung verwendet, die es diesem Kollektor im Gegensatz zum Durchsatzkollektor ermöglicht, mit dem gleichzeitigen Low-Pause-Kollektor zusammenzuarbeiten.

Die Kehrseite der Medaille ist, dass der Garbage Collector mit Durchsatz (-XX:+UseParallelGC) zwar mit adaptiver Größenbestimmung (-XX:+UseAdaptiveSizePolicy) verwendet werden kann, der parallele Young Generation Collector (-XX:+UseParNewGC) jedoch nicht.

-XX:+UseAdaptiveSizePolicy zeichnet Statistiken über GC-Zeiten, Allokationsraten und freien Speicherplatz auf und passt dann die Größe der jungen und dauerhaften Generationen an diese Statistiken an. Diese Option ist für die Verwendung mit dem Durchsatzkollektor vorgesehen und standardmäßig aktiviert.

You Might Also Like

Wie ein Elektronikriese Ingenieure dort trifft, wo sie sind – mit 44 Millionen Produkten im Katalog

Lernen Sie Mohammad Mahboob kennen: Ein Direktor der Suchplattform, der 44 Millionen...

Read More

Geschützt: Von der Suche zu Lösungen: Wie KI-Agenten den digitalen Handel im Jahr 2025 antreiben können

Es gibt keinen Textauszug, da dies ein geschützter Beitrag ist.

Read More

Individuelle KI-Agenten erstellen, ohne eine einzige Zeile Code zu schreiben? Ja, das haben wir getan.

Endlich eine Low-Code-KI-Plattform (wirklich kein Code), mit der die Menschen, die Ihre...

Read More

Quick Links