Salta al contenuto principale

12 - JVM e Performance Tuning

Architettura della JVM, aree di memoria, Garbage Collection, JIT compiler, profiling, diagnostica e ottimizzazione delle performance.

Cosa ne pensi di questo articolo?

In Breve

Architettura della JVM, aree di memoria, Garbage Collection, JIT compiler, profiling, diagnostica e ottimizzazione delle performance.

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
Class Loading Process
// 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)
Stack vs Heap
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

Generazioni 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.

Oggetti Elegibili per GC
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
Configurazione GC
// 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.

JIT Compilation Tiers
// 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

Strumenti di Profiling
// 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

Cause Comuni di Memory Leak
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

Best Practices Performance
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
Configurazione Produzione
# 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.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti