So wärmen Sie die JVM auf

1. Übersicht

Die JVM ist eine der ältesten und leistungsstärksten virtuellen Maschinen, die jemals gebaut wurden.

In diesem Artikel sehen wir uns kurz an, was es bedeutet, eine JVM aufzuwärmen und wie es geht.

2. Grundlagen der JVM-Architektur

Bei jedem Start eines neuen JVM-Prozesses werden alle erforderlichen Klassen von einer Instanz des ClassLoader in den Speicher geladen. Dieser Vorgang erfolgt in drei Schritten:

  1. Laden der Bootstrap-Klasse: Der „ Bootstrap Class Loader “ lädt Java-Code und wichtige Java-Klassen wie java.lang.Object in den Speicher. Diese geladenen Klassen befinden sich in JRE \ lib \ rt.jar .
  2. Laden der Erweiterungsklasse : Der ExtClassLoader ist für das Laden aller JAR-Dateien verantwortlich, die sich im Pfad java.ext.dirs befinden . In Nicht-Maven- oder Nicht-Gradle-basierten Anwendungen, in denen ein Entwickler JARs manuell hinzufügt, werden alle diese Klassen in dieser Phase geladen.
  3. Laden von Anwendungsklassen : Der AppClassLoader lädt alle Klassen, die sich im Pfad der Anwendungsklasse befinden.

Dieser Initialisierungsprozess basiert auf einem Lazy-Loading-Schema.

3. Was erwärmt die JVM?

Sobald das Laden der Klasse abgeschlossen ist, werden alle wichtigen Klassen (die zum Zeitpunkt des Prozessstarts verwendet wurden) in den JVM-Cache (nativer Code) verschoben, wodurch sie zur Laufzeit schneller zugänglich sind. Andere Klassen werden pro Anforderung geladen.

Die erste Anforderung an eine Java-Webanwendung ist häufig wesentlich langsamer als die durchschnittliche Antwortzeit während der Lebensdauer des Prozesses. Diese Aufwärmphase kann normalerweise auf das verzögerte Laden von Klassen und das Just-in-Time-Kompilieren zurückgeführt werden.

Vor diesem Hintergrund müssen wir für Anwendungen mit geringer Latenz alle Klassen im Voraus zwischenspeichern, damit sie beim Zugriff zur Laufzeit sofort verfügbar sind.

Dieser Vorgang zum Einstellen der JVM wird als Aufwärmen bezeichnet.

4. Stufenweise Zusammenstellung

Dank der Soundarchitektur der JVM werden häufig verwendete Methoden während des Anwendungslebenszyklus in den nativen Cache geladen.

Wir können diese Eigenschaft verwenden, um kritische Methoden beim Start einer Anwendung zwangsweise in den Cache zu laden. Insofern müssen wir ein VM-Argument mit dem Namen Tiered Compilation festlegen :

-XX:CompileThreshold -XX:TieredCompilation

Normalerweise verwendet die VM den Interpreter, um Profilinformationen zu Methoden zu sammeln, die in den Compiler eingegeben werden. In dem gestuften Schema wird der Client-Compiler zusätzlich zum Interpreter verwendet, um kompilierte Versionen von Methoden zu generieren, die Profilinformationen über sich selbst sammeln.

Da kompilierter Code wesentlich schneller als interpretierter Code ist, wird das Programm während der Profilierungsphase mit besserer Leistung ausgeführt.

Anwendungen, die auf JBoss und JDK Version 7 mit aktiviertem VM-Argument ausgeführt werden, stürzen nach einiger Zeit aufgrund eines dokumentierten Fehlers ab. Das Problem wurde in JDK Version 8 behoben.

Ein weiterer Punkt, den Sie hier beachten sollten, ist, dass wir zum Erzwingen des Ladens sicherstellen müssen, dass auf alle (oder die meisten) Klassen, die ausgeführt werden sollen, zugegriffen werden muss. Dies ähnelt der Bestimmung der Codeabdeckung beim Testen von Einheiten. Je mehr Code abgedeckt ist, desto besser ist die Leistung.

Der nächste Abschnitt zeigt, wie dies implementiert werden kann.

5. Manuelle Implementierung

Wir können eine alternative Technik implementieren, um die JVM aufzuwärmen. In diesem Fall kann ein einfaches manuelles Aufwärmen das tausendfache Wiederholen der Erstellung verschiedener Klassen umfassen, sobald die Anwendung gestartet wird.

Zunächst müssen wir eine Dummy-Klasse mit einer normalen Methode erstellen:

public class Dummy { public void m() { } }

Als Nächstes müssen wir eine Klasse erstellen, die über eine statische Methode verfügt, die mindestens 100000 Mal ausgeführt wird, sobald die Anwendung gestartet wird. Bei jeder Ausführung wird eine neue Instanz der zuvor erstellten Dummy-Klasse erstellt:

public class ManualClassLoader { protected static void load() { for (int i = 0; i < 100000; i++) { Dummy dummy = new Dummy(); dummy.m(); } } }

Um den Leistungsgewinn zu messen , müssen wir nun eine Hauptklasse erstellen. Diese Klasse enthält einen statischen Block, der einen direkten Aufruf der load () -Methode von ManualClassLoader enthält .

Innerhalb der Hauptfunktion rufen wir die load () -Methode des ManualClassLoader erneut auf und erfassen die Systemzeit in Nanosekunden unmittelbar vor und nach unserem Funktionsaufruf. Schließlich subtrahieren wir diese Zeiten, um die tatsächliche Ausführungszeit zu erhalten.

Wir müssen die Anwendung zweimal ausführen. einmal mit dem Methodenaufruf load () innerhalb des statischen Blocks und einmal ohne diesen Methodenaufruf:

public class MainApplication { static { long start = System.nanoTime(); ManualClassLoader.load(); long end = System.nanoTime(); System.out.println("Warm Up time : " + (end - start)); } public static void main(String[] args) { long start = System.nanoTime(); ManualClassLoader.load(); long end = System.nanoTime(); System.out.println("Total time taken : " + (end - start)); } }

Nachfolgend sind die Ergebnisse in Nanosekunden wiedergegeben:

Mit Aufwärmen Kein Aufwärmen Unterschied(%)
1220056 8903640 730
1083797 13609530 1256
1026025 9283837 905
1024047 7234871 706
868782 9146180 1053

Wie erwartet zeigt der Aufwärmansatz eine viel bessere Leistung als der normale.

Dies ist natürlich ein sehr vereinfachter Benchmark und bietet nur einen oberflächlichen Einblick in die Auswirkungen dieser Technik. Es ist auch wichtig zu verstehen, dass wir uns bei einer realen Anwendung mit den typischen Codepfaden im System aufwärmen müssen.

6. Werkzeuge

We can also use several tools to warm up the JVM. One of the most well-known tools is the Java Microbenchmark Harness, JMH. It's generally used for micro-benchmarking. Once it is loaded, it repeatedly hits a code snippet and monitors the warm-up iteration cycle.

To use it we need to add another dependency to the pom.xml:

 org.openjdk.jmh jmh-core 1.19   org.openjdk.jmh jmh-generator-annprocess 1.19 

We can check the latest version of JMH in Central Maven Repository.

Alternatively, we can use JMH's maven plugin to generate a sample project:

mvn archetype:generate \ -DinteractiveMode=false \ -DarchetypeGroupId=org.openjdk.jmh \ -DarchetypeArtifactId=jmh-java-benchmark-archetype \ -DgroupId=com.baeldung \ -DartifactId=test \ -Dversion=1.0

Next, let's create a main method:

public static void main(String[] args) throws RunnerException, IOException { Main.main(args); }

Now, we need to create a method and annotate it with JMH's @Benchmark annotation:

@Benchmark public void init() { //code snippet }

Inside this init method, we need to write code that needs to be executed repeatedly in order to warm up.

7. Performance Benchmark

In the last 20 years, most contributions to Java were related to the GC (Garbage Collector) and JIT (Just In Time Compiler). Almost all of the performance benchmarks found online are done on a JVM already running for some time. However,

However, Beihang University has published a benchmark report taking into account JVM warm-up time. They used Hadoop and Spark based systems to process massive data:

Here HotTub designates the environment in which the JVM was warmed up.

As you can see, the speed-up can be significant, especially for relatively small read operations – which is why this data is interesting to consider.

8. Conclusion

In this quick article, we showed how the JVM loads classes when an application starts and how we can warm up the JVM in order gain a performance boost.

In diesem Buch werden weitere Informationen und Richtlinien zum Thema behandelt, wenn Sie fortfahren möchten.

Und wie immer ist der vollständige Quellcode auf GitHub verfügbar.