CodeGym /Corsi /JAVA 25 SELF /Profilazione e ottimizzazione del codice: strumenti, appr...

Profilazione e ottimizzazione del codice: strumenti, approcci

JAVA 25 SELF
Livello 63 , Lezione 4
Disponibile

1. Introduzione alla profilazione

La profilazione è come un check-up medico per il tuo programma: non guardiamo solo la “temperatura” (monitoraggio), ma cerchiamo dove l'applicazione “fa male”, cosa funziona lentamente, dove si consuma troppa memoria o risorse.

La profilazione è il processo di raccolta e analisi di informazioni sul funzionamento del programma con l'obiettivo di individuare colli di bottiglia (bottlenecks) e parti di codice inefficienti. A differenza del monitoraggio, che di solito tiene traccia di indicatori generali (carico della CPU, memoria, numero di thread), la profilazione permette di guardare all'interno: scoprire quali metodi vengono chiamati più spesso, quanto tempo richiedono, quanti oggetti vengono creati e dove avvengono le perdite di memoria.

Quando la profilazione è davvero necessaria?

  • L'applicazione “rallenta”, ma non è chiaro perché.
  • Il consumo di memoria è aumentato all'improvviso.
  • Dopo un aggiornamento del codice qualcosa ha iniziato a funzionare più lentamente.
  • Serve capire perché al server non bastano le risorse.

A proposito, quasi ogni sviluppatore almeno una volta ha ottimizzato la parte sbagliata del codice. Perché? Perché “a occhio” è praticamente impossibile individuare il collo di bottiglia — per questo serve un profilatore.

Metriche principali della profilazione

  • Tempo di esecuzione dei metodi (profilazione CPU): Quali metodi impiegano più tempo? Dove il programma “consuma” CPU?
  • Uso della memoria (profilazione della memoria): Quali oggetti vengono creati più spesso? Dove restano in memoria più a lungo del necessario?
  • Numero di oggetti: Stiamo creando troppi oggetti dello stesso tipo?
  • Thread: Ci sono troppi thread? Ci sono blocchi (deadlock, contention)?
  • Chiamate di metodo: Qual è la profondità dello stack? Avviene una ricorsione senza uscita?

2. Strumenti di profilazione

Nel mondo Java ci sono diversi strumenti classici (e gratuiti!) che permettono di effettuare la profilazione. Consideriamo i principali.

VisualVM

VisualVM è uno strumento gratuito, incluso nel JDK (a partire da JDK 6). Consente di:

  • Collegarsi a JVM locali e remote.
  • Osservare memoria, thread, CPU, garbage collection.
  • Eseguire un heap dump e analizzarlo.
  • Profilare l'applicazione per CPU e memoria.

Come avviare VisualVM?
Di solito si trova nella cartella del JDK: <percorso_al_JDK>/bin/jvisualvm
Lo avvii, scegli il processo dell'applicazione Java — e puoi osservarne la vita come pesci in un acquario (solo che qui i pesci sono oggetti e thread).

JProfiler, YourKit

Sono strumenti commerciali, ma molto potenti. Consentono di:

  • Profilare memoria, CPU, thread.
  • Analizzare gli “snapshot” di memoria (heap dump).
  • Individuare leak, lock prolungati, metodi lenti.
  • Integrarsi con IDE e CI/CD.

Per iniziare basta VisualVM, ma se passerai a progetti più grandi — prendi in considerazione questi strumenti.

Java Flight Recorder (JFR)

JFR è uno strumento integrato nel JDK per raccogliere eventi sull'attività della JVM. È molto leggero, quasi non influisce sulle prestazioni, e consente di raccogliere informazioni su:

  • Tempo di esecuzione dei metodi.
  • Garbage collection.
  • Thread, lock, errori.

JFR è perfetto per l'uso in produzione, quando non si può rallentare l'applicazione.

3. Pratica: profiliamo una semplice applicazione

Creiamo un mini calcolatore che sappia eseguire calcoli lunghi e conservare la cronologia delle operazioni (così avremo cicli, collezioni e lavoro con la memoria).

Esempio di codice: “Calcolatore lento”

import java.util.ArrayList;
import java.util.List;

public class SlowCalculator {
    private final List<String> history = new ArrayList<>();

    public int add(int a, int b) {
        simulateHeavyOperation();
        int result = a + b;
        history.add(a + " + " + b + " = " + result);
        return result;
    }

    public int multiply(int a, int b) {
        simulateHeavyOperation();
        int result = a * b;
        history.add(a + " * " + b + " = " + result);
        return result;
    }

    public List<String> getHistory() {
        return history;
    }

    // Simulazione di un'operazione "pesante"
    private void simulateHeavyOperation() {
        for (int i = 0; i < 5_000_000; i++) {
            Math.sqrt(i);
        }
    }
}

E ora — la classe principale:

public class Main {
    public static void main(String[] args) {
        SlowCalculator calc = new SlowCalculator();
        for (int i = 0; i < 10; i++) {
            calc.add(i, i * 2);
            calc.multiply(i, i + 5);
        }
        System.out.println("Cronologia delle operazioni:");
        for (String entry : calc.getHistory()) {
            System.out.println(entry);
        }
    }
}

Come profilare questa applicazione?

  1. Compila ed esegui l'applicazione.
  2. Apri VisualVM (jvisualvm).
  3. Trova il tuo processo (di solito per nome della classe Main).
  4. Vai alla scheda CPU Profiler e fai clic su Start.
  5. Lascia che il programma lavori (oppure premi di nuovo il pulsante lento).
  6. Guarda quali metodi occupano più tempo.

Domanda: Secondo te, quale metodo sarà il più “pesante”?
Risposta: Naturalmente simulateHeavyOperation() — perché esegue un enorme ciclo da 5_000_000 iterazioni e chiama Math.sqrt.

4. Problemi tipici di prestazioni

Algoritmi lenti

La causa più comune: scelta sbagliata dell'algoritmo o della struttura dati. Per esempio, cercare in una lista invece di usare HashMap, o l'ordinamento a bolla invece del quicksort.

Esempio:

// Ricerca lenta
for (String s : list) {
    if (s.equals("target")) {
        // trovato
    }
}

Meglio usare Set o Map per una ricerca veloce.

Memory leak

Un memory leak è una situazione in cui gli oggetti rimangono “vivi” (esistono riferimenti a essi), anche se non servono più. Questo porta a un aumento del consumo di memoria e, alla fine, a OutOfMemoryError.

public class MemoryLeakDemo {
    private static List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) {
        while (true) {
            leakyList.add(new byte[1_000_000]); // 1 MB
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        }
    }
}

Come trovare i leak?
Esegui un heap dump in VisualVM e guarda quali oggetti occupano più memoria e perché esistono riferimenti a essi.

Creazione eccessiva di oggetti

Se in un ciclo crei molti oggetti dello stesso tipo, non solo carichi il garbage collector, ma potresti anche rallentare l'applicazione.

for (int i = 0; i < 1_000_000; i++) {
    String s = new String("hello"); // male!
}

Meglio usare costanti o il pool delle stringhe (String pool).

Blocchi tra thread

Se più thread si contendono la stessa risorsa (per esempio, un metodo sincronizzato), ciò può portare a lock e a un calo delle prestazioni.

public synchronized void doWork() {
    // ...
}

Come individuarli?
Nella scheda Threads di VisualVM puoi vedere quali thread sono “bloccati” e perché.

5. Approcci all'ottimizzazione

Prima misura, poi ottimizza

Regola principale dell'ottimizzazione: Non ottimizzare ciò che non rallenta.

Per prima cosa profila, trova i “punti caldi”, e solo dopo modifica il codice. A volte la parte di codice più “ovvia” occupa solo 1 % del tempo, mentre il vero “mostro” è da qualche parte in una libreria o in un punto inatteso.

Uso del profilatore per trovare i punti caldi

Un punto caldo è un metodo o una porzione di codice che occupa la quota maggiore del tempo di esecuzione dell'applicazione.

In VisualVM è visibile nella scheda CPU Profiler:

  • Ordina i metodi per tempo di esecuzione.
  • Guarda lo stack trace: chi chiama chi.
  • Ricorda che a volte il “colpevole” non è il tuo codice, ma una libreria o persino il JDK.

Esempi di ottimizzazione

Esempio 1: Sostituzione dell'algoritmo
Se hai visto che si spende più tempo nella ricerca in una lista, sostituisci List con HashSet.

Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
    // veloce!
}

Esempio 2: Riduzione del numero di allocazioni
Invece di creare nuovi oggetti nel ciclo, usa il riutilizzo o StringBuilder.

// Male:
for (int i = 0; i < 10000; i++) {
    String s = "Risultato: " + i;
}

// Meglio:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.setLength(0);
    sb.append("Risultato: ").append(i);
    String s = sb.toString();
}

Esempio 3: Caching
Se vedi che un metodo pesante viene chiamato molte volte con gli stessi parametri, usa una cache.

Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
    return sqrtCache.computeIfAbsent(x, Math::sqrt);
}

6. Dimostrazione: velocizziamo il nostro calcolatore

Problema: simulateHeavyOperation() consuma molto tempo

Passo 1. Profiliamo
In VisualVM si vede che quasi tutto il tempo va su Math.sqrt(i) dentro un ciclo da 5_000_000 iterazioni.

Passo 2. Ottimizziamo
Se è solo una simulazione di carico — rimuovila o riduci il numero di iterazioni.
Se è logica di business reale — valuta se puoi:

  • Mettere in cache il risultato.
  • Usare un algoritmo più veloce.
  • Spostare i calcoli in un thread separato (se non è critico per l'utente).

Esempio di ottimizzazione:

private void simulateHeavyOperation() {
    // Era 5_000_000, diventato 100_000
    for (int i = 0; i < 100_000; i++) {
        Math.sqrt(i);
    }
}

Passo 3. Verifica del risultato
Eseguiamo di nuovo la profilazione — il programma gira più veloce e il carico sulla CPU è diminuito.

7. Visualizzazione: il processo di ottimizzazione

flowchart TD
    A[Avvio dell'applicazione]
    B["Profilazione (VisualVM)"]
    C[Individuazione dei colli di bottiglia]
    D[Ottimizzazione del codice]
    E[Profilazione ripetuta]
    F[Miglioramento delle prestazioni]

    A --> B --> C --> D --> E --> F
    E --> C

8. Errori tipici nella profilazione e ottimizzazione

Errore n. 1: Ottimizzare “a occhio”. Molto spesso gli sviluppatori iniziano a cambiare il codice senza misurare dove sia davvero il problema. Il risultato — tanto lavoro, effetti minimi.

Errore n. 2: Profilazione in condizioni “non realistiche”. Bisogna profilare su dati e carico simili a quelli reali. La profilazione “a vuoto” può non rivelare i veri problemi.

Errore n. 3: Ignorare i memory leak. Se non guardi l'heap dump e non analizzi i riferimenti, potresti non accorgerti a lungo che il programma “si gonfia” e sta per andare in crash.

Errore n. 4: Inseguire micro-ottimizzazioni. Non vale la pena spendere giorni per velocizzare il codice che occupa solo 0.1 % del tempo di esecuzione dell'applicazione. Prima — i colli di bottiglia principali.

Errore n. 5: Non considerare thread e sincronizzazione. Nelle applicazioni multithread i problemi di prestazioni spesso sono legati non agli algoritmi, ma a lock e attese (synchronized, contention).

Errore n. 6: Dimenticarsi della profilazione dopo le modifiche. Dopo l'ottimizzazione verifica necessariamente il risultato: a volte la “ottimizzazione” può perfino rallentare l'esecuzione!

1
Sondaggio/quiz
Logging, livello 63, lezione 4
Non disponibile
Logging
Logging, monitoring e profiling
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION