JVM e Performance Tuning
La Java Virtual Machine (JVM) è il cuore di ogni applicazione Java. Comprenderne il funzionamento interno e le tecniche di ottimizzazione permette di scrivere applicazioni più performanti e diagnosticare problemi di memoria.
Cosa Imparerai
- Architettura della JVM
- Aree di memoria: Stack, Heap, Metaspace
- Garbage Collection e algoritmi
- JVM tuning con flag
- Profiling e diagnostica
- Memory leak e ottimizzazione
Architettura della JVM
La JVM è composta da tre componenti principali: Class Loader, Runtime Data Areas e Execution Engine.
Componenti Principali della JVM
| Componente | Funzione |
|---|---|
| Class Loader | Carica, collega e inizializza le classi |
| Runtime Data Areas | Stack, Heap, Method Area, PC Register |
| Execution Engine | Interprete, JIT Compiler, Garbage Collector |
// Il Class Loading avviene in 3 fasi:
// 1. Loading - Legge il bytecode (.class)
// 2. Linking - Verifica, Prepara, Risolve
// 3. Initialization - Esegue static blocks e inizializzazioni
public class ClassLoadingDemo {
// Static block eseguito durante initialization
static {
System.out.println("Classe caricata e inizializzata!");
}
public static void main(String[] args) {
// Ottiene il ClassLoader corrente
ClassLoader loader = ClassLoadingDemo.class.getClassLoader();
System.out.println("ClassLoader: " + loader);
// Gerarchia dei ClassLoader:
// 1. Bootstrap ClassLoader (classi core Java)
// 2. Platform/Extension ClassLoader
// 3. Application/System ClassLoader (classpath)
// Caricamento dinamico di una classe
try {
Class<?> clazz = Class.forName("java.util.ArrayList");
System.out.println("Classe caricata: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Aree di Memoria
La JVM gestisce diverse aree di memoria, ognuna con uno scopo specifico.
Runtime Data Areas
| Area | Contenuto | Condivisa |
|---|---|---|
| Heap | Oggetti e array | Sì (tra tutti i thread) |
| Stack | Frame dei metodi, variabili locali | No (uno per thread) |
| Metaspace | Metadata classi, method area | Sì |
| PC Register | Indirizzo istruzione corrente | No (uno per thread) |
| Native Method Stack | Stack per metodi nativi | No (uno per thread) |
public class MemoryAreasDemo {
// Variabile di istanza -> HEAP (dentro l'oggetto)
private int instanceVar = 10;
// Variabile statica -> METASPACE
private static String staticVar = "static";
public void metodo() {
// Variabili locali primitive -> STACK
int localPrimitive = 5;
// Reference locale -> STACK, oggetto -> HEAP
String localString = new String("hello");
// Array: reference in STACK, contenuto in HEAP
int[] array = new int[10];
}
public static void main(String[] args) {
// Ogni chiamata a metodo crea un nuovo Stack Frame
// Stack Frame contiene:
// - Variabili locali
// - Operand Stack
// - Reference al constant pool
MemoryAreasDemo demo = new MemoryAreasDemo();
// demo (reference) -> STACK
// new MemoryAreasDemo() (oggetto) -> HEAP
demo.metodo();
}
}
Struttura dell'Heap
// L'Heap è diviso in generazioni:
// YOUNG GENERATION (Eden + Survivor Spaces)
// - Oggetti appena creati vanno qui
// - Minor GC frequenti e veloci
// - Diviso in: Eden, Survivor 0 (S0), Survivor 1 (S1)
// OLD GENERATION (Tenured)
// - Oggetti sopravvissuti a più GC cycles
// - Major GC meno frequenti ma più lenti
// - Oggetti grandi possono andare direttamente qui
// Flag per dimensionare l'heap:
// -Xms512m Heap iniziale (512 MB)
// -Xmx2g Heap massimo (2 GB)
// -Xmn256m Young Generation (256 MB)
// Esempio di configurazione:
// java -Xms1g -Xmx4g -Xmn512m MyApplication
// Visualizzare memoria a runtime:
public class HeapDemo {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory(); // -Xmx
long totalMemory = runtime.totalMemory(); // Heap corrente
long freeMemory = runtime.freeMemory(); // Libera nel corrente
long usedMemory = totalMemory - freeMemory;
System.out.println("Max Memory: " + maxMemory / (1024 * 1024) + " MB");
System.out.println("Total Memory: " + totalMemory / (1024 * 1024) + " MB");
System.out.println("Free Memory: " + freeMemory / (1024 * 1024) + " MB");
System.out.println("Used Memory: " + usedMemory / (1024 * 1024) + " MB");
}
}
Garbage Collection
Il Garbage Collector (GC) libera automaticamente la memoria occupata da oggetti non più raggiungibili.
public class GCDemo {
public static void main(String[] args) {
// 1. Nullifying reference
Object obj1 = new Object();
obj1 = null; // Ora elegibile per GC
// 2. Reassigning reference
Object obj2 = new Object(); // Primo oggetto
obj2 = new Object(); // Primo oggetto ora elegibile
// 3. Objects inside methods
creaOggetti(); // Oggetti creati nel metodo elegibili dopo return
// 4. Island of isolation
class Nodo {
Nodo next;
}
Nodo a = new Nodo();
Nodo b = new Nodo();
a.next = b;
b.next = a;
a = null;
b = null;
// Entrambi elegibili (nessun reference esterno)
// Suggerire GC (non garantito!)
System.gc();
Runtime.getRuntime().gc();
}
static void creaOggetti() {
Object local = new Object();
// local elegibile per GC dopo che il metodo termina
}
}
Algoritmi di Garbage Collection
Garbage Collectors Disponibili
| GC | Flag | Caratteristiche |
|---|---|---|
| Serial GC | -XX:+UseSerialGC | Single thread, per applicazioni piccole |
| Parallel GC | -XX:+UseParallelGC | Multi-thread, ottimizza throughput |
| G1 GC | -XX:+UseG1GC | Default Java 9+, basse pause, heap grandi |
| ZGC | -XX:+UseZGC | Pause ultra-basse (<10ms), heap enormi |
| Shenandoah | -XX:+UseShenandoahGC | Pause basse, concurrent compaction |
// Flag comuni per GC tuning:
// Scegliere il GC
// -XX:+UseG1GC (default Java 9+)
// -XX:+UseZGC (pause ultra-basse)
// -XX:+UseParallelGC (throughput)
// G1 GC tuning
// -XX:MaxGCPauseMillis=200 Target pausa max (default 200ms)
// -XX:G1HeapRegionSize=4m Dimensione regioni (1-32 MB)
// -XX:InitiatingHeapOccupancyPercent=45 Soglia concurrent GC
// GC Logging (Java 9+)
// -Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10m
// Esempio completo:
// java -Xms2g -Xmx4g \
// -XX:+UseG1GC \
// -XX:MaxGCPauseMillis=100 \
// -Xlog:gc*:file=gc.log \
// MyApplication
// Statistiche GC a runtime:
import java.lang.management.*;
import java.util.List;
public class GCStatsDemo {
public static void main(String[] args) {
List<GarbageCollectorMXBean> gcBeans =
ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gc : gcBeans) {
System.out.println("GC Name: " + gc.getName());
System.out.println("Collection Count: " + gc.getCollectionCount());
System.out.println("Collection Time: " + gc.getCollectionTime() + " ms");
System.out.println();
}
}
}
JIT Compilation
Il Just-In-Time (JIT) Compiler compila il bytecode in codice nativo a runtime, ottimizzando le parti più eseguite del codice.
// La JVM usa Tiered Compilation (default):
// Tier 0: Interprete puro
// Tier 1: C1 compiler (compilazione veloce, poche ottimizzazioni)
// Tier 2: C1 con profiling limitato
// Tier 3: C1 con profiling completo
// Tier 4: C2 compiler (ottimizzazione massima)
// Flag JIT:
// -XX:+TieredCompilation (default on)
// -XX:TieredStopAtLevel=1 Ferma a C1 (startup veloce)
// -XX:-TieredCompilation Solo C2 (server apps)
// -XX:CompileThreshold=10000 Soglia per compilazione
// Vedere cosa viene compilato:
// -XX:+PrintCompilation
// Esempio output:
// 123 1 3 java.lang.String::hashCode (55 bytes)
// 125 2 4 java.util.HashMap::hash (20 bytes)
// Colonne: timestamp, id, tier, metodo, dimensione bytecode
// Verificare ottimizzazioni:
public class JitDemo {
public static void main(String[] args) {
// Loop caldo - sarà compilato da JIT
long sum = 0;
for (int i = 0; i < 100_000; i++) {
sum += calcola(i);
}
System.out.println("Sum: " + sum);
}
// Questo metodo verrà inlined dal JIT
static int calcola(int n) {
return n * 2 + 1;
}
}
Profiling e Diagnostica
// 1. jconsole / jvisualvm - GUI monitoring
// Avviare con: jconsole oppure jvisualvm
// 2. jps - Lista processi Java
// $ jps -l
// 12345 com.example.MyApp
// 3. jstat - Statistiche GC
// $ jstat -gc <pid> 1000 (ogni 1000ms)
// $ jstat -gcutil <pid> 1000
// 4. jmap - Memory dump
// $ jmap -histo <pid> Lista oggetti
// $ jmap -dump:file=heap.hprof <pid> Heap dump
// 5. jstack - Thread dump
// $ jstack <pid>
// 6. Java Flight Recorder (JFR)
// $ java -XX:StartFlightRecording=duration=60s,filename=recording.jfr MyApp
// Analisi programmatica memoria:
import java.lang.management.*;
public class MemoryProfilingDemo {
public static void main(String[] args) {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
// Heap memory
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
System.out.println("Heap:");
System.out.println(" Init: " + heapUsage.getInit() / (1024*1024) + " MB");
System.out.println(" Used: " + heapUsage.getUsed() / (1024*1024) + " MB");
System.out.println(" Committed: " + heapUsage.getCommitted() / (1024*1024) + " MB");
System.out.println(" Max: " + heapUsage.getMax() / (1024*1024) + " MB");
// Non-heap (Metaspace)
MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
System.out.println("Non-Heap:");
System.out.println(" Used: " + nonHeapUsage.getUsed() / (1024*1024) + " MB");
// Thread info
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
System.out.println("Thread count: " + threadBean.getThreadCount());
System.out.println("Peak thread count: " + threadBean.getPeakThreadCount());
}
}
Memory Leak e Ottimizzazione
import java.util.*;
public class MemoryLeakExamples {
// 1. LEAK: Static collections che crescono
private static List<byte[]> cache = new ArrayList<>();
public void addToCache(byte[] data) {
cache.add(data); // Mai rimosso!
}
// FIX: Usare cache con limite
// private static LRUCache cache = new LRUCache(100);
// 2. LEAK: Listener non rimossi
class Button {
private List<ActionListener> listeners = new ArrayList<>();
void addListener(ActionListener l) {
listeners.add(l);
}
// Manca removeListener()!
}
interface ActionListener { void onClick(); }
// 3. LEAK: Inner class mantiene reference all'outer
class Outer {
byte[] bigData = new byte[10_000_000];
class Inner {
// Mantiene implicitamente reference a Outer!
}
Inner createInner() {
return new Inner();
}
}
// FIX: Usare static inner class
// static class Inner { ... }
// 4. LEAK: Connessioni/risorse non chiuse
public void readFile() {
try {
// FileInputStream fis = new FileInputStream("file.txt");
// Leggi...
// Manca fis.close()!
} catch (Exception e) {
e.printStackTrace();
}
}
// FIX: try-with-resources
public void readFileSafe() {
// try (FileInputStream fis = new FileInputStream("file.txt")) {
// // Leggi...
// } // Chiuso automaticamente
}
// 5. LEAK: HashMap con chiavi mutabili
public void hashMapLeak() {
Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>();
key.add("a");
map.put(key, "valore");
key.add("b"); // Modifica il hashCode!
// Ora map.get(key) non trova più l'entry
// L'entry rimane "orfana" nella mappa
}
}
Tecniche di Ottimizzazione
import java.util.*;
public class PerformanceOptimizations {
// 1. StringBuilder per concatenazioni in loop
public String buildStringBad(int n) {
String result = "";
for (int i = 0; i < n; i++) {
result += i; // Crea nuovo String ogni volta!
}
return result;
}
public String buildStringGood(int n) {
StringBuilder sb = new StringBuilder(n * 4); // Pre-alloca
for (int i = 0; i < n; i++) {
sb.append(i);
}
return sb.toString();
}
// 2. Inizializzare collezioni con capacità
public void collectionCapacity() {
// BAD: resize multipli
List<String> list = new ArrayList<>();
// GOOD: pre-allocata
List<String> listGood = new ArrayList<>(1000);
// Stesso per HashMap (considera load factor 0.75)
Map<String, Integer> map = new HashMap<>((int)(1000 / 0.75) + 1);
}
// 3. Evitare boxing/unboxing in loop
public long sumBad(List<Integer> numbers) {
long sum = 0;
for (Integer n : numbers) {
sum += n; // Unboxing ad ogni iterazione
}
return sum;
}
public long sumGood(int[] numbers) {
long sum = 0;
for (int n : numbers) {
sum += n; // Nessun boxing
}
return sum;
}
// 4. Lazy initialization
private volatile ExpensiveObject expensive;
public ExpensiveObject getExpensive() {
if (expensive == null) {
synchronized (this) {
if (expensive == null) {
expensive = new ExpensiveObject();
}
}
}
return expensive;
}
class ExpensiveObject {}
// 5. Object pooling per oggetti pesanti
// Esempio: connection pool per database
}
Flag JVM Comuni
Flag di Configurazione
| Flag | Descrizione |
|---|---|
| -Xms, -Xmx | Heap iniziale e massimo |
| -Xss | Dimensione stack thread (es. -Xss512k) |
| -XX:MetaspaceSize | Metaspace iniziale |
| -XX:+HeapDumpOnOutOfMemoryError | Dump automatico su OOM |
| -XX:HeapDumpPath | Path per heap dump |
| -XX:+PrintFlagsFinal | Mostra tutti i flag |
# Esempio configurazione per applicazione web in produzione:
java \
-Xms4g \
-Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m \
-XX:+ExitOnOutOfMemoryError \
-jar myapp.jar
# Flag per container (Docker/Kubernetes):
java \
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-jar myapp.jar
Best Practices
Regole per Performance Ottimali
- Misura prima di ottimizzare: usa profiler
- Dimensiona heap correttamente: né troppo né poco
- Scegli il GC giusto: G1 per applicazioni generiche
- Monitora GC: abilita logging
- Evita memory leak: chiudi risorse, rimuovi listener
- Preferisci primitive: evita boxing inutile
- Pre-alloca collezioni: specifica capacità iniziale
- Usa try-with-resources: per gestire risorse
Domande frequenti
Quali sono i tre componenti principali della Java Virtual Machine (JVM)?
I tre componenti principali della JVM sono il Class Loader, le Runtime Data Areas e l'Execution Engine.
Come funziona la gestione delle aree di memoria nella JVM?
La JVM gestisce diverse aree di memoria, ognuna con uno scopo specifico, come ad esempio l'heap per lo storage degli oggetti.
Qual è il ruolo del Garbage Collector (GC) nella JVM?
Il GC libera automaticamente la memoria occupata da oggetti non più raggiungibili, evitando memory leak e migliorando le prestazioni dell'applicazione.
Che cosa fa il Just-In-Time (JIT) Compiler nella JVM?
Il JIT Compiler compila il bytecode in codice nativo a runtime, ottimizzando le parti più eseguite del codice e migliorando le prestazioni dell'applicazione.

Commenti
Caricamento commenti...
Accedi per commentare