1. Errori tipici nella gestione della memoria
È ora di guardare il rovescio della medaglia della magia della gestione automatica della memoria. Anche se non scrivete in C, dove bisogna controllare ogni byte personalmente, in Java si possono combinare pasticci tali che l’applicazione divora memoria come un gatto affamato con la salsiccia. Vediamo gli errori più comuni e come evitarli.
Listener dimenticati (listeners)
In Java è spesso usato il pattern «listener» — un oggetto che si iscrive agli eventi di un altro oggetto. Per esempio, avete creato un pulsante e gli avete aggiunto un gestore di clic:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// gestione del clic
}
});
Problema: se vi dimenticate di rimuovere questo listener (removeActionListener) quando il pulsante o la finestra non servono più, il listener rimane appeso in memoria. Anche se avete chiuso la finestra e azzerato tutti i riferimenti ad essa, l’oggetto-listener mantiene ancora un riferimento alla finestra (o viceversa), impedendo al garbage collector di liberare memoria.
Analogia: Immaginate di esservi trasferiti, ma di aver dimenticato di disiscrivervi dalla newsletter della pizzeria: continuano a mandarvi pubblicità al vecchio indirizzo.
Collezioni statiche che non vengono pulite
I campi statici vivono quanto la classe (e talvolta — fino alla fine della vita dell’applicazione). Se avete una collezione statica:
public class Cache {
public static final List<String> globalList = new ArrayList<>();
}
e ci aggiungete oggetti ma non li rimuovete, rimarranno in memoria per sempre. Anche se non esistono più altri riferimenti a quegli oggetti, il riferimento dalla collezione statica non permetterà al GC di eliminarli.
Esempio reale: cache di foto in un’app desktop che non viene mai ripulita. Dopo un paio d’ore di utilizzo — OutOfMemoryError.
Mancata liberazione delle risorse (file, stream, connessioni)
Sebbene Java liberi la memoria, non si occupa della chiusura automatica dei descrittori di file, delle connessioni di rete e di altre risorse esterne. Se vi dimenticate di chiudere un file o uno stream, la risorsa rimarrà occupata e, a un certo punto, il sistema dirà: «Basta, non fornirò più file!» (IOException: Too many open files).
Consiglio: usate sempre try-with-resources:
try (FileInputStream in = new FileInputStream("data.txt")) {
// leggiamo il file
} // in.close() verrà chiamato automaticamente!
Oggetti grandi che rimangono in memoria a lungo
A volte create un grande array o una collezione, lo usate e poi «vi dimenticate di rilasciarlo». Per esempio:
List<byte[]> bigList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
bigList.add(new byte[1024 * 1024]); // 1 MB ciascuno
}
// ... abbiamo dimenticato di svuotare bigList
Se questa collezione vive in un campo statico o in un oggetto che non viene eliminato per molto tempo, tutta quella memoria resterà occupata.
Classi interne e anonime: cattura di riferimenti esterni
Le classi anonime (e interne) in Java conservano un riferimento implicito all’oggetto esterno:
public class Outer {
void doSomething() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello from inner!");
}
};
// r viene salvato da qualche parte
}
}
Se l’oggetto r finisce in una collezione statica o in una cache, «tratterrà» un riferimento all’istanza di Outer, anche se questa non serve più. Risultato — leak di memoria. Con le espressioni lambda la situazione è un po’ migliore, ma se la lambda usa i campi della classe esterna, il riferimento viene comunque conservato.
2. Errori nell’uso del garbage collector
Chiamata forzata a System.gc()
Molti principianti pensano: «La memoria sta finendo — chiamo System.gc() e si risolverà tutto!». In realtà è solo una richiesta alla JVM, non una garanzia di raccolta immediata. Un utilizzo frequente può peggiorare drasticamente le prestazioni, causando pause lunghe e freeze. Nelle applicazioni reali è meglio fidarsi della JVM — deciderà lei quando raccogliere. Tra l’altro, alcune JVM possono ignorare le chiamate esplicite al GC (ad esempio con l’opzione -XX:+DisableExplicitGC).
Ignorare i log del GC
Nei log del GC si vede quando avvengono le raccolte, quanto tempo impiegano e quanta memoria viene liberata. Se non si guardano questi log, si possono perdere segnali di problemi: pause lunghe, Full GC frequenti, leak di memoria.
Come abilitare i log del GC:
java -Xlog:gc* -jar MyApp.jar
oppure per JVM più vecchie:
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar MyApp.jar
Scelta errata del GC per il caso d’uso
La scelta del collector influisce su latenza e stabilità. Per bassa latenza (borse, giochi online) il parallel stop-the-world Parallel GC è una cattiva idea: può «congelare» tutti i thread durante la raccolta. Considerate G1 GC, ZGC o Shenandoah.
3. Errori con le collezioni
Uso di HashMap invece di WeakHashMap per le cache.
Se fate una cache in cui gli oggetti devono essere rimossi automaticamente quando non ci sono più riferimenti «vivi», usate WeakHashMap:
Map<Key, Value> cache = new WeakHashMap<>();
Con una normale HashMap gli oggetti vivranno fino a quando la cache non verrà pulita manualmente, il che porterà a leak di memoria.
Chiamate a remove() dimenticate per gli elementi.
Se aggiungete oggetti alle collezioni (ad esempio agli elenchi dei listener), ma non li rimuovete quando non servono più, questi oggetti vivranno per sempre, soprattutto nelle collezioni di lunga vita (ad esempio statiche).
4. Best practice: come evitare i problemi
Rimuovete sempre i listener.
Se un oggetto si è sottoscritto agli eventi, assicuratevi di cancellarlo quando non serve più. È comodo farlo nel metodo dispose() o alla chiusura della finestra/schermo.
button.removeActionListener(myListener);
Usate riferimenti deboli per le cache.
Se nella cache si può fare a meno della garanzia di conservazione dell’oggetto, usate WeakReference o collezioni basate su di essi (WeakHashMap). Così il GC potrà liberare memoria quando necessario.
Monitorate la memoria in produzione.
Usate jvisualvm, jconsole o sistemi APM. Questo aiuta a intercettare i leak prima delle lamentele degli utenti.
Analizzate l’heap dump in caso di sospetto leak
Se l’applicazione ha iniziato a «divorare» più memoria del solito, acquisite un heap dump (ad esempio tramite jmap o jvisualvm) e verificate quali oggetti occupano più spazio. Spesso il colpevole si trova in pochi minuti.
Configurate i parametri della JVM.
- -Xmx — dimensione massima dell’heap
- -Xms — dimensione iniziale dell’heap
Limiti ragionevoli aiutano a evitare OutOfMemoryError e accelerano la diagnosi.
5. Pratica: esempio di codice con memory leak e sua correzione
Esempio 1: Leak tramite collezione statica
public class MemoryLeakDemo {
// Collezione statica — vive per sempre
private static final List<byte[]> leakyList = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
leakyList.add(new byte[1024 * 1024]); // 1 MB ogni volta
System.out.println("Aggiunti " + (i + 1) + " MB");
}
// OutOfMemoryError!
}
}
Correzione: usate una variabile locale oppure pulite la collezione quando non serve più.
public class MemoryLeakFixed {
public static void main(String[] args) {
List<byte[]> tempList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
tempList.add(new byte[1024 * 1024]);
System.out.println("Aggiunti " + (i + 1) + " MB");
}
// tempList = null; // Si può azzerare esplicitamente
// Ora gli oggetti sono disponibili per il GC dopo l’uscita dal metodo
}
}
Esempio 2: Leak tramite listener
public class Window {
private final List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener l) {
listeners.add(l);
}
// Non c'è il metodo removeListener!
}
Correzione: aggiungete un metodo per rimuovere il listener e chiamatelo alla chiusura della finestra.
public void removeListener(EventListener l) {
listeners.remove(l);
}
Esempio 3: Cache con HashMap invece di WeakHashMap
Map<Object, Object> cache = new HashMap<>();
// ... aggiungiamo oggetti
Correzione: passate a WeakHashMap:
Map<Object, Object> cache = new WeakHashMap<>();
Suggerimenti per configurare la JVM per il monitoraggio della memoria
- Abilitate i log del GC: -Xlog:gc* o -XX:+PrintGCDetails
- Limitate la dimensione massima dell’heap: -Xmx512m
- Se usate cache, controllatene la dimensione e applicate riferimenti deboli quando è appropriato
- Sperimentate con il GC: -XX:+UseG1GC, -XX:+UseZGC, -XX:+UseShenandoahGC
7. Errori tipici nella gestione della memoria
Errore n. 1: listener e sottoscrizioni dimenticati. Se avete aggiunto un listener a un oggetto ma vi siete dimenticati di rimuoverlo, l’oggetto-listener (e tutto ciò a cui fa riferimento) rimarrà in memoria. Un classico per GUI e sistemi event-driven. Usate removeListener/removeActionListener.
Errore n. 2: collezioni statiche senza pulizia. I campi statici vivono più a lungo. Se ci mettete dentro oggetti e non pulite la collezione, quegli oggetti resteranno in memoria per sempre. Particolarmente insidioso per cache senza limiti.
Errore n. 3: mancata liberazione delle risorse esterne. Avete lasciato aperto uno stream, un file o una connessione? Perdete memoria e arrivate ai limiti del sistema operativo. Usate try-with-resources e chiudete le risorse.
Errore n. 4: chiamata forzata a System.gc(). Non è una panacea, ma solo una richiesta alla JVM. Spesso porta a pause e degrado delle prestazioni.
Errore n. 5: collezioni normali per le cache. Se gli oggetti nella cache devono essere rimossi automaticamente, usate riferimenti weak/soft (WeakHashMap, SoftReference). Altrimenti avrete un leak.
Errore n. 6: classi interne e anonime che catturano riferimenti esterni. Le classi interne e le lambda possono trattenere implicitamente un riferimento all’oggetto esterno. Se vengono salvate in una collezione di lunga vita — è un leak.
Errore n. 7: ignorare i log del GC. Se non guardate i log del GC, non scoprirete pause lunghe o Full GC frequenti — gli utenti lo capiranno dai rallentamenti e freeze. Abilitate -Xlog:gc* o -XX:+PrintGCDetails.
GO TO FULL VERSION