Come riscaldare la JVM

1. Panoramica

La JVM è una delle macchine virtuali più vecchie ma potenti mai costruite.

In questo articolo, diamo una rapida occhiata a cosa significa riscaldare una JVM e come farlo.

2. Nozioni di base sull'architettura JVM

Ogni volta che viene avviato un nuovo processo JVM, tutte le classi richieste vengono caricate in memoria da un'istanza di ClassLoader. Questo processo si svolge in tre fasi:

  1. Caricamento classi Bootstrap: il " Caricatore classi Bootstrap " carica il codice Java e le classi Java essenziali come java.lang.Object nella memoria. Queste classi caricate risiedono in JRE \ lib \ rt.jar .
  2. Caricamento della classe di estensione : ExtClassLoader è responsabile del caricamento di tutti i file JAR situati nel percorso java.ext.dirs . Nelle applicazioni non basate su Maven o non Gradle, in cui uno sviluppatore aggiunge manualmente i JAR, tutte quelle classi vengono caricate durante questa fase.
  3. Caricamento della classe dell'applicazione : AppClassLoader carica tutte le classi che si trovano nel percorso della classe dell'applicazione.

Questo processo di inizializzazione si basa su uno schema di caricamento lento.

3. Cosa sta riscaldando la JVM

Una volta completato il caricamento della classe, tutte le classi importanti (utilizzate al momento dell'avvio del processo) vengono inserite nella cache JVM (codice nativo), il che le rende più accessibili durante il runtime. Altre classi vengono caricate in base alla richiesta.

La prima richiesta effettuata a un'applicazione Web Java è spesso sostanzialmente più lenta del tempo di risposta medio durante la durata del processo. Questo periodo di riscaldamento può essere solitamente attribuito al caricamento lento delle classi e alla compilazione just-in-time.

Tenendo presente questo, per le applicazioni a bassa latenza, dobbiamo prima memorizzare nella cache tutte le classi, in modo che siano disponibili immediatamente quando si accede al runtime.

Questo processo di messa a punto della JVM è noto come riscaldamento.

4. Compilazione a più livelli

Grazie alla solida architettura della JVM, i metodi utilizzati di frequente vengono caricati nella cache nativa durante il ciclo di vita dell'applicazione.

Possiamo utilizzare questa proprietà per forzare il caricamento di metodi critici nella cache all'avvio di un'applicazione. A tal fine, è necessario impostare un argomento VM denominato Compilazione a livelli :

-XX:CompileThreshold -XX:TieredCompilation

Normalmente, la VM utilizza l'interprete per raccogliere informazioni di profilazione sui metodi che vengono immessi nel compilatore. Nello schema a più livelli, oltre all'interprete, il compilatore client viene utilizzato per generare versioni compilate di metodi che raccolgono informazioni di profilo su se stessi.

Poiché il codice compilato è sostanzialmente più veloce del codice interpretato, il programma viene eseguito con prestazioni migliori durante la fase di profilazione.

Le applicazioni in esecuzione su JBoss e JDK versione 7 con questo argomento VM abilitato tendono a bloccarsi dopo un po 'di tempo a causa di un bug documentato. Il problema è stato risolto nella versione 8 di JDK.

Un altro punto da notare qui è che per forzare il caricamento, dobbiamo assicurarci che sia necessario accedere a tutte (o alla maggior parte) delle classi che verranno eseguite. È simile alla determinazione della copertura del codice durante i test unitari. Più codice è coperto, migliori saranno le prestazioni.

La sezione successiva mostra come implementarlo.

5. Implementazione manuale

Possiamo implementare una tecnica alternativa per riscaldare la JVM. In questo caso, un semplice riscaldamento manuale potrebbe includere la ripetizione della creazione di classi diverse migliaia di volte non appena viene avviata l'applicazione.

Innanzitutto, dobbiamo creare una classe fittizia con un metodo normale:

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

Successivamente, dobbiamo creare una classe che abbia un metodo statico che verrà eseguito almeno 100000 volte non appena l'applicazione viene avviata e ad ogni esecuzione, crea una nuova istanza della suddetta classe fittizia che abbiamo creato in precedenza:

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

Ora, per misurare il guadagno di prestazioni , dobbiamo creare una classe principale. Questa classe contiene un blocco statico che contiene una chiamata diretta al metodo load () di ManualClassLoader .

All'interno della funzione principale, effettuiamo di nuovo una chiamata al metodo load () del ManualClassLoader e acquisiamo l' ora di sistema in nanosecondi subito prima e dopo la nostra chiamata di funzione. Infine, sottraiamo questi tempi per ottenere il tempo di esecuzione effettivo.

Dobbiamo eseguire l'applicazione due volte; una volta con la chiamata al metodo load () all'interno del blocco statico e una volta senza questa chiamata al metodo:

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)); } }

Di seguito i risultati sono riprodotti in nanosecondi:

Con Warm Up Nessun riscaldamento Differenza(%)
1220056 8903640 730
1083797 13609530 1256
1026025 9283837 905
1024047 7234871 706
868782 9146180 1053

Come previsto, con il riscaldamento l'approccio mostra prestazioni molto migliori rispetto a quella normale.

Naturalmente, questo è un benchmark molto semplicistico e fornisce solo alcune informazioni a livello superficiale sull'impatto di questa tecnica. Inoltre, è importante capire che, con un'applicazione del mondo reale, dobbiamo riscaldarci con i percorsi di codice tipici nel sistema.

6. Strumenti

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.

Questo libro fornisce ulteriori informazioni e linee guida sull'argomento se desideri continuare.

E, come sempre, il codice sorgente completo è disponibile su GitHub.