Hoe de JVM opwarmen

1. Overzicht

De JVM is een van de oudste maar krachtige virtuele machines die ooit zijn gebouwd.

In dit artikel bekijken we kort wat het betekent om een ​​JVM op te warmen en hoe u dit moet doen.

2. Basisprincipes van JVM-architectuur

Telkens wanneer een nieuw JVM-proces start, worden alle vereiste klassen in het geheugen geladen door een instantie van de ClassLoader. Dit proces verloopt in drie stappen:

  1. Bootstrap-klasse laden: De "Bootstrap Class Loader”Laadt Java-code en essentiële Java-klassen zoals java.lang.Object in het geheugen. Deze geladen klassen bevinden zich in JRE \ lib \ rt.jar.
  2. Uitbreidingsklasse laden: De ExtClassLoader is verantwoordelijk voor het laden van alle JAR-bestanden op het java.ext.dirs pad. In niet-Maven- of niet-Gradle-gebaseerde applicaties, waar een ontwikkelaar handmatig JAR's toevoegt, worden al die klassen tijdens deze fase geladen.
  3. Toepassingsklasse laden: De AppClassLoader laadt alle klassen die zich in het toepassingsklassenpad bevinden.

Dit initialisatieproces is gebaseerd op een lazy loading-schema.

3. Wat verwarmt de JVM

Zodra het laden van de klassen is voltooid, worden alle belangrijke klassen (gebruikt bij het starten van het proces) in de JVM-cache (native code) gepusht, waardoor ze tijdens runtime sneller toegankelijk zijn. Andere klassen worden per verzoek geladen.

Het eerste verzoek aan een Java-webapplicatie is vaak aanzienlijk langzamer dan de gemiddelde responstijd gedurende de levensduur van het proces. Deze opwarmperiode kan meestal worden toegeschreven aan het laden van luie lessen en just-in-time-compilatie.

Dit in gedachten houdend, moeten we voor toepassingen met een lage latentie alle klassen van tevoren in de cache opslaan, zodat ze onmiddellijk beschikbaar zijn wanneer ze tijdens runtime worden geopend.

Dit proces van het afstemmen van de JVM staat bekend als opwarmen.

4. Gelaagde compilatie

Dankzij de degelijke architectuur van de JVM worden veelgebruikte methoden tijdens de levenscyclus van de applicatie in de native cache geladen.

We kunnen deze eigenschap gebruiken om kritieke methoden geforceerd in de cache te laden wanneer een applicatie start. In dat opzicht moeten we een VM-argument met de naam instellen Gelaagde compilatie:

-XX: CompileThreshold -XX: TieredCompilation

Normaal gesproken gebruikt de VM de interpreter om profileringsinformatie te verzamelen over methoden die in de compiler worden ingevoerd. In het gelaagde schema wordt, naast de interpreter, de client-compiler gebruikt om gecompileerde versies van methoden te genereren die profileringsinformatie over zichzelf verzamelen.

Omdat gecompileerde code aanzienlijk sneller is dan geïnterpreteerde code, wordt het programma tijdens de profileringsfase met betere prestaties uitgevoerd.

Applicaties die draaien op JBoss en JDK versie 7 met dit VM-argument ingeschakeld, hebben de neiging om na enige tijd te crashen vanwege een gedocumenteerde bug. Het probleem is opgelost in JDK-versie 8.

Een ander punt om op te merken is dat om het laden te forceren, we ervoor moeten zorgen dat alle (of de meeste) klassen die worden uitgevoerd, moeten worden geopend. Het is vergelijkbaar met het bepalen van codedekking tijdens het testen van eenheden. Hoe meer code wordt afgedekt, hoe beter de prestaties zullen zijn.

De volgende sectie laat zien hoe dit kan worden geïmplementeerd.

5. Handmatige implementatie

We kunnen een alternatieve techniek implementeren om de JVM op te warmen. In dit geval zou een eenvoudige handmatige opwarming kunnen bestaan ​​uit het duizenden keren herhalen van het maken van verschillende klassen zodra de toepassing start.

Ten eerste moeten we een dummy-klasse maken met een normale methode:

openbare klas Dummy {openbare leegte m () {}}

Vervolgens moeten we een klasse maken met een statische methode die minstens 100.000 keer wordt uitgevoerd zodra de toepassing start en bij elke uitvoering een nieuwe instantie van de bovengenoemde dummy-klasse die we eerder hebben gemaakt:

openbare klasse ManualClassLoader {beschermde statische ongeldige belasting () {voor (int i = 0; i <100000; i ++) {Dummy dummy = nieuwe Dummy (); dummy.m (); }}}

Nu, om meet de prestatiewinst, we moeten een hoofdklasse maken. Deze klasse bevat één statisch blok met een directe aanroep naar de ManualClassLoader's belasting () methode.

Binnen de hoofdfunctie bellen we naar de ManualClassLoader's belasting () methode nogmaals en leg de systeemtijd vast in nanoseconden net voor en na onze functieaanroep. Ten slotte trekken we deze tijden af ​​om de werkelijke uitvoeringstijd te krijgen.

We moeten de applicatie twee keer draaien; eenmaal met de laden() method call binnen het statische blok en eenmaal zonder deze method call:

openbare klasse MainApplication {statische {lange start = System.nanoTime (); ManualClassLoader.load (); lange einde = System.nanoTime (); System.out.println ("Opwarmtijd:" + (einde - start)); } openbare statische leegte hoofd (String [] args) {lange start = System.nanoTime (); ManualClassLoader.load (); lange einde = System.nanoTime (); System.out.println ("Totale benodigde tijd:" + (end - start)); }}

Hieronder worden de resultaten weergegeven in nanoseconden:

Met OpwarmenGeen opwarmingVerschil(%)
1220056 8903640 730
1083797 13609530 1256
1026025 9283837 905
1024047 7234871 706
868782 9146180 1053

Zoals verwacht, vertoont de opwarmbenadering veel betere prestaties dan de normale.

Dit is natuurlijk een zeer simplistische benchmark en geeft slechts enig oppervlakkig inzicht in de impact van deze techniek. Het is ook belangrijk om te begrijpen dat we met een real-world applicatie moeten opwarmen met de typische codepaden in het systeem.

6. Gereedschap

We kunnen ook verschillende tools gebruiken om de JVM op te warmen. Een van de bekendste tools is de Java Microbenchmark Harness, JMH. Het wordt over het algemeen gebruikt voor micro-benchmarking. Zodra het is geladen, het raakt herhaaldelijk een codefragment en bewaakt de opwarmcyclus.

Om het te gebruiken, moeten we een andere afhankelijkheid toevoegen aan het pom.xml:

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

We kunnen de nieuwste versie van JMH bekijken in Central Maven Repository.

Als alternatief kunnen we de maven-plug-in van JMH gebruiken om een ​​voorbeeldproject te genereren:

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

Laten we vervolgens een hoofd methode:

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

Nu moeten we een methode maken en deze annoteren met JMH's @Benchmark annotatie:

@Benchmark public void init () {// codefragment}

Hierbinnen in het methode, moeten we code schrijven die herhaaldelijk moet worden uitgevoerd om op te warmen.

7. Prestatiebenchmark

In de afgelopen 20 jaar waren de meeste bijdragen aan Java gerelateerd aan de GC (Garbage Collector) en JIT (Just In Time Compiler). Bijna alle prestatiebenchmarks die online worden gevonden, worden gedaan op een JVM die al een tijdje actief is. Echter,

Echter, Beihang University heeft een benchmarkrapport gepubliceerd dat rekening houdt met de opwarmtijd van JVM. Ze gebruikten op Hadoop en Spark gebaseerde systemen om enorme gegevens te verwerken:

Hier geeft HotTub de omgeving aan waarin de JVM werd opgewarmd.

Zoals u kunt zien, kan de versnelling aanzienlijk zijn, vooral voor relatief kleine leesbewerkingen - daarom zijn deze gegevens interessant om te overwegen.

8. Conclusie

In dit korte artikel hebben we laten zien hoe de JVM klassen laadt wanneer een applicatie start en hoe we de JVM kunnen opwarmen om een ​​prestatieverbetering te krijgen.

Dit boek bevat meer informatie en richtlijnen over het onderwerp als u wilt doorgaan.

En, zoals altijd, is de volledige broncode beschikbaar op GitHub.