Diese Webseite ist nicht kompatibel mit Ihrem Browser. Wechseln Sie zu einer aktuellen Version von  ChromeFirefox  oder Safari, um diese fehlerfrei nutzen zu können.
X

Optimierung von JVM-Einstellungen: Energieeffizienz und Ressourcennutzung verbessern

Von: Julius Liebau, David Kopp

19. März 2025

Java JVM-Settings Energieeffizienz Java-Virtual-Machine

Die Java Virtual Machine (JVM) ist das Fundament jeder Java-Anwendung und bietet zahlreiche Konfigurationsmöglichkeiten, um den Energieverbrauch zu optimieren und Ressourcen effizient zu nutzen. Im Anschluss an den Blog-Artikel Energieeffizienz durch JVM-Auswahl: Relevanz der richtigen Distribution wird hier beleuchtet, wie bestimmte JVM-Einstellungen die Energieeffizienz beeinflussen können.

Dieser Artikel soll einen Überblick über die wichtigsten Optimierungen geben und somit verdeutlichen, welche Möglichkeiten es grundsätzlich gibt, um die Energieeffizienz positiv zu beeinflussen.
Für den Betrieb von JVMs in container-basierten Umgebungen (z. B. Kubernetes) sind einige zusätzliche Aspekte zu beachten, die wir in einem zukünftigen Blog-Artikel behandeln werden.

Die Ausführungen stützen sich im Wesentlichen auf die empirischen Untersuchungen von Mohammed Chakib Belgaid aus dem Jahr 2022, der seine Doktorarbeit dem Thema "Green coding" gewidmet hat. Weitere wichtige Quellen für uns sind das Buch "Optimizing Cloud Native Java" von Benjamin J. Evans & James Gough aus dem Jahr 2024 (O'Reilly) sowie die Talks von Bruno Borges zum Thema "Secrets of Performance Tuning Java on Kubernetes" (YouTube).

Um die Energieeffizienz einer Java-Anwendung zu optimieren, spielen mehrere JVM-Einstellungen eine wesentliche Rolle:

  • Garbage Collection (GC): Die Wahl des richtigen Garbage Collectors kann sich maßgeblich auf den Energieverbrauch und die Ressourcennutzung auswirken.
  • Heap-Größe: Eine angemessene Heap-Größe kann den Energieverbrauch reduzieren. Zu große Heaps führen zu unnötigem Energieverbrauch, während zu kleine Heaps häufigere Garbage Collections verursachen.
  • Thread-Management: Effiziente Thread-Verwaltung minimiert Synchronisationskosten und reduziert unnötigen Energieverbrauch.
  • Just-in-Time-Kompilierung (JIT): Optimiert Code dynamisch während der Laufzeit, mit Auswirkungen auf die Performance und Energieeffizienz.

Garbage Collection (GC) optimieren

Die Garbage Collection beeinflusst maßgeblich die Energieeffizienz einer JVM. Jede Anwendung nutzt den Speicher auf individuelle Weise, wodurch unterschiedliche Anforderungen an die Garbage Collection entstehen. In den umfangreichen Untersuchungen von Belgaid zum Energieverhalten der Garbage Collection wurde deutlich, dass es keine universelle Lösung gibt. Die optimale Konfiguration hängt von Faktoren wie GC-Dauer, Durchsatz, Speichergröße und der Anzahl der GC-Threads ab. Experimente sind unerlässlich, um die effizienteste Einstellung für die eigene Anwendung zu finden. Obwohl Standard-GC-Einstellungen oft solide Ergebnisse liefern, zeigte sich in 50% der Experimente, dass angepasste Konfigurationen den Energieverbrauch erheblich reduzieren können. Eine auf die Anwendung abgestimmte GC-Auswahl und Optimierung ist daher von entscheidender Bedeutung.

Garbage Collectors, ihre Charakteristiken und Stärken

Überblick über die wichtigsten GCs:

  • Serial GC: Nutzt nur einen Thread und setzt auf "Stop-the-World"-Pausen. Eignet sich für Systeme mit nur einem CPU-Kern und Anwendungen mit kleinen Heaps.
  • Parallel GC: Nutzt im Gegensatz zum Serial GC mehrere Threads, ist ansonsten jedoch vergleichbar und benötigt ebenfalls "Stop-the-World"-Pausen (alle Applikationsthreads müssen pausiert werden). Eignet sich für Multi-Kern-Systeme und Anwendungen mit kleinen Heaps.
  • G1 GC: Im Vergleich zu Serial und Parallel GC insbesondere optimierte Pausenzeiten. Bietet insgesamt ein ausgewogenes Verhältnis zwischen Pausenzeiten und Durchsatz. Liefert gute Ergebnisse für allgemeine Workloads (Request-Response mit DB-Interaktionen).
  • ZGC: Entwickelt für Anwendungen mit großen Heaps und hohen Latenzanforderungen. Bietet extrem kurze GC-Pausenzeiten auf Millisekunden-Niveau, jedoch mit hohem Initialaufwand.
  • Shenandoah GC: Reduziert Pausenzeiten durch parallele und inkrementelle Speicherbereinigung. Zusätzlicher Overhead durch erhöhte Thread-Koordination. Geeignet für latenzkritische Anwendungen.

Obwohl die Garbage Collectors Serial und Parallel bereits sehr alt sind – Serial GC wurde mit JDK 1.3 im Jahr 2000 veröffentlicht, Parallel GC mit JDK 1.4 in 2002 – sind sie auch heute (Jahr 2025) immer noch relevant für die Praxis und werden auch noch weiterentwickelt (siehe z. B. Änderungen am Parallel GC in JDK 23).

Standardmäßig kommt bei HotSpot-JVMs in Version 11 oder neuer entweder Serial GC oder G1 GC zum Einsatz, je nachdem wie viele Ressourcen bereitstehen:

Verfügbare Ressourcen GC
1 CPU Serial
<1792 MB RAM Serial
>1 CPU und >=1792 MB RAM G1

In containerbasierten Umgebungen, in denen typischerweise einem Container vergleichsweise wenig Ressourcen bereitgestellt werden, kommt somit häufig standardmäßig der Serial GC zum Einsatz (und nicht G1 GC). Dies dürfte für manche überraschend sein. Das Thema JVM in containerbasierten Umgebungen wird in einem zukünftigen Blog-Artikel ausführlicher diskutiert.

Welcher GC zum Einsatz kommt, lässt sich explizit über Parameter festlegen:
-XX:+UseSerialGC, XX:+UseParallelGC, -XX:+UseG1GC, -XX:UseZGC, -XX:+UseShenandoahGC.

Empfehlung:

Es ist nicht möglich eine allgemeingültige Empfehlung zu geben. Die folgenden Überlegungen können jedoch als Startpunkt angesehen werden, von wo aus weitere Untersuchungen (siehe nächster Abschnitt) stattfinden sollten.
Moderne GCs wie ZGC oder Shenandoah eignen sich vor allem für Systeme mit viel Ressourcen (>4GB Memory) und bei denen eine geringe Latenz von Relevanz ist.
G1 GC ist für typische webbasierte Workloads wie Request-Reply-Anwendungen häufig die sinnvollste Option.
Bei Systemen mit wenig Ressourcen (<4 GB Memory) und bei denen keine strikten Latenzanforderungen existieren, kann Parallel GC aufgrund seines hohen Durchsatzes die effizienteste Option darstellen.

Logging und Feinabstimmung mit GC-Parametern

Um Optimierungspotentiale zu identifizieren, ist Transparenz nötig, wann und wie lange der GC aktiv ist. Hierfür sollte das Protokollieren von GC-Ereignissen aktiviert werden. Die Protokollierung der Garbage Collection in der JVM hat einen sehr geringen Overhead, so dass es durchaus sinnvoll ist, GC-Logging in allen JVMs, insbesondere in Produktion, zu aktivieren.

GC-Logging kann via JVM-Flags aktiviert werden: -Xlog:gc oder -Xlog:gc* für mehr Details.
Mit dem Kommando java -Xlog:help können alle verfügbaren Parameter angezeigt werden.

Es gibt zahlreiche Parameter, um den Garbage Collector perfekt an die Bedürfnisse der eigenen Workloads anzupassen. Eine Beschreibung aller Parameter sprengt den Rahmen dieses Blog-Artikels.
Als Beispiel sollen deshalb nur zwei Parameter genannt werden:

  • XX:MaxGCPauseMillis=<time>: Begrenzung der maximalen GC-Pausenzeiten. Default beim G1 GC sind 200 ms. Eine Anpassung ist nur relevant bei großen Heaps und Anwendungen mit strikten Latenzanforderungen.
  • XX:GCTimeRatio=<N>: Steuert das Verhältnis zwischen GC-Aufwand und Anwendungszeit. Eine Feinabstimmung kann besonders in speicherintensiven Anwendungen sinnvoll sein.

Weitere relevante Parameter und Informationen zur Optimierung der Garbage Collection können beispielsweise im HotSpot Virtual Machine Garbage Collection Tuning Guide von Oracle gefunden werden.

Heap-Größe anpassen

Die Heap-Größe beeinflusst den Energieverbrauch und die Performance: Ein zu kleiner Heap führt potentiell zu Performanceeinbußen und häufigeren GC-Zyklen, während ein zu großer Heap unnötig Speicherressourcen beansprucht. Je mehr Speicherressourcen benötigt bzw. in der Cloud reserviert werden, desto höher ist der Energieverbrauch sowie die anteilig zu berechenbaren Embodied-Carbon-Emissionen (aufgrund der Herstellung der IT-Komponenten).
Eine Abstimmung kann die Effizienz deutlich verbessern.

Wie viel die JVM an Memory für den Heap allokiert hängt von der Menge am insgesamt verfügbarem Memory ab. Bei aktuellen Hotspot-JVMs gilt die folgende Regel:

Verfügbarer RAM Heap-Größe
<256 MB 50%
256-512 MB 127 MB
>512 MB 25%

Es mag überraschen, dass die JVM bei mehr als 512 MB verfügbarem Speicher standardmäßig nur 25% für den Heap reserviert. Warum ist das der Fall? Neben Java-Objekten benötigt die JVM Speicher für native Strukturen wie Thread-Stacks, Code-Caches und Metadaten. Eine zu große Heap-Zuweisung könnte zudem andere Prozesse verdrängen und das System destabilisieren. Die moderate Voreinstellung sorgt für eine stabile Basis, ohne sofortige manuelle Anpassungen zu erfordern.

Allerdings führt diese Standardeinstellung in modernen (container-basierten) Umgebungen zu ineffizienter Speichernutzung. Ein Beispiel: Läuft eine JVM-Anwendung in einem Kubernetes-Container mit einem Memory-Limit von 1024 MB, stehen dem Heap nur 256 MB zur Verfügung. Der Rest – oft mehrere hundert Megabyte – bleibt größtenteils ungenutzt.

Deshalb sollte die Heap-Größe, insbesondere in containerbasierten Workloads, unbedingt angepasst werden, um die Speichernutzung zu optimieren. Allerdings gilt das nicht für alle Anwendungen: Dienste wie Apache Spark oder Elasticsearch haben einen hohen Off-Heap-Verbrauch, sodass hier eine zu großzügige Heap-Zuweisung vermieden werden sollte.

Es gibt mehrere Möglichkeiten die Heap-Größe anzupassen.

Heap-Größe absolut einstellen:

  • Xms<size>: Initiale Heap-Größe
  • Xmx<size>: Maximale Heap-Größe

Heap-Größe prozentual einstellen:

  • -XX:InitialRAMPercentage: Initiale Heap-Größe in Prozent
  • -XX:MaxRAMPercentage: Maximale Heap-Größe in Prozent

Empfehlung:

Die Heap-Größe sollte explizit an die Bedürfnisse der eigenen Anwendung angepasst werden und nicht auf die Standardkonfiguration gesetzt werden! Statt nur 25% für den Heap zu verwenden, sollte für typische Workloads eher ein Wert von 60-80% für die Heap-Größe in Betracht gezogen werden. Mithilfe von Profiling- und Observability-Tools sollten die Anforderungen bzgl. Speicherverbrauch und Heap-Größe der eigenen Anwendung und der JVM ermittelt und entsprechende Optimierungen vorgenommen werden.

Thread-Management optimieren

Die Thread-Verwaltung ist besonders bei parallelen Anwendungen ein entscheidender Faktor, um eine hohe Effizienz zu ermöglichen.
In der Green coding-Arbeit konnte Belgaid zeigen, dass OpenJ9 aufgrund seines effizienteren Thread-Managements im Avrora-Benchmark signifikant weniger Laufzeit benötigt und Energie verbraucht als andere JVMs. Beim Avrora-Benchmark handelt es sich um eine Simulation von AVR-Mikrocontrollern, bei der es sehr stark auf Multi-Threading ankommt. IBM OpenJ9 nutzt Thread Local Heaps (TLHs), was den Synchronisationsaufwand reduziert, Deadlocks vermeidet, Cache-Lokalität verbessert, schnellere Speicherallokation ermöglicht und Fragmentierung minimiert, wodurch die Speicherverwaltung insgesamt effizienter und performanter wird.

Mit der Einführung von Virtual Threads in Java 19 ergeben sich weitere Möglichkeiten, die Energieeffizienz und Parallelität zu verbessern. Virtual Threads bieten eine skalierbare Alternative zu herkömmlichen Plattform-Threads, da sie leichtgewichtig sind und weniger Ressourcen beanspruchen. Für Anwendungen mit hoher Nebenläufigkeit können Virtual Threads die Anzahl blockierender Threads reduzieren und so den Energieverbrauch senken.

Empfehlung:

  • Anwendungen mit hoher Parallelität profitieren von JVMs wie OpenJ9, die optimierte Thread-Management-Strategien nutzen, sowie von der Nutzung von Virtual Threads. Dadurch lassen sich Blockierungen minimieren und Ressourcen effizienter verwenden.
  • Bei Workloads mit geringer Last sollte die Thread-Anzahl reduziert werden, um Kontextwechsel zu minimieren. Übermäßige Parallelisierung bei Workloads mit geringer Auslastung kann zu unnötigem Energieverbrauch führen.

Just-in-Time-Kompilierung (JIT)

Die JIT-Kompilierung optimiert den Bytecode zur Laufzeit in Maschinencode, was die Leistung gegenüber der Interpretation massiv steigert und damit in der Regel auch die Energieeffizienz verbessert:

  • Standardkonfigurationen der JIT liefern in 80% der Fälle optimale Ergebnisse (siehe Belgaid).
  • Einstellungen wie "VeryHot" oder "Scorching" erhöhen die Performance, führen jedoch zu einem höheren Energieverbrauch und eignen sich nur für spezielle Workloads.

Empfehlung:

Die Standard-JIT-Konfiguration ist in den meisten Fällen ausreichend. Anpassungen sollten nur vorgenommen werden, wenn spezifische Workloads das erfordern.

Sind Standardkonfigurationen ausreichend oder lohnt sich eine Optimierung?

Die Standardkonfigurationen moderner JVMs bieten zwar eine solide Basis, jedoch kann eine gezielte Optimierung zu beachtlichen Energieeinsparungen führen:

  • Garbage Collection: Die Auswahl einer für den Workload passenden GC-Implementierung sowie ggf. darüber hinausgehende Feinabstimmungen können Energieverbrauch und Pausenzeiten signifikant senken.
  • Heap-Konfiguration: Eine präzise Anpassung verringert die Anzahl der GC-Zyklen und verbessert die Speicherauslastung signifikant.
  • Thread-Management: Effiziente Einstellungen optimieren die CPU-Auslastung, reduzieren Kontextwechsel und Synchronisationskosten.

Tools wie JReferral (mehr dazu in unserem ersten Blog-Artikel zur JVM-Auswahl) erleichtern die energieeffizienteste JVM-Konfiguration zu finden und anzuwenden.

Bereits innerhalb der Entwicklungsumgebung können Profiling-Tools wie VisualVM dabei helfen, verschiedene JVM-relevante Aspekte wie Speicherverbräuche und GC-Aktivierungen zu veranschaulichen und darauf basierend Optimierungen vorzunehmen.
In Produktion sollten Observability-Lösungen zum Einsatz kommen, die eine Echtzeit-Überwachung von JVM-Metriken wie Heap-Nutzung, Garbage Collection und Thread-Performance ermöglichen. Dadurch lassen sich Engpässe erkennen und Optimierungspotentiale identifizieren.

Fazit

Die richtige Konfiguration der JVM kann den Energieverbrauch und die Ressourceneffizienz von Java-Anwendungen erheblich verbessern. Während Standardkonfigurationen häufig eine solide Grundlage bieten, lohnt es sich, Anwendungen individuell zu testen und Parameter wie den Garbage Collector, die Heap-Größe und das Thread-Management anzupassen. Mit Tools wie J-Referral lassen sich geeignete Optimierungen identifizieren. Zudem sollten Profiling- und Observability-Tools eingesetzt werden, um Engpässe und Probleme frühzeitig zu erkennen. Optimierungen an der JVM-Konfiguration schonen nicht nur die Umwelt, sondern senken auch die Kosten.

Quellen

Zur Übersicht