1. Dove risiedono le variabili locali
Iniziamo dalla cosa più semplice: le variabili locali. Sono variabili dichiarate all’interno di un metodo ed esistono solo durante la sua esecuzione. La loro vita è breve e drammatica: non appena il metodo termina, tutte le sue variabili locali scompaiono senza lasciare traccia.
In Java le variabili locali sono archiviate nello stack. Ogni thread ha il proprio stack. Se un metodo chiama un altro metodo, nello stack viene aggiunto un nuovo «frame» (stack frame) con le variabili locali e l’indirizzo di ritorno. Quando il metodo termina, il suo frame viene rimosso dallo stack.
Esempio: la vita di una variabile locale
public class LocalVariableDemo {
public static void main(String[] args) {
int a = 42; // la variabile locale a vive solo in main
printSquare(a);
// Qui la variabile b non c'è più!
}
public static void printSquare(int b) {
int square = b * b; // variabile locale square
System.out.println("Quadrato: " + square);
// Dopo l'uscita da printSquare tutte le variabili locali scompaiono
}
}
Punto importante: se una variabile locale è un riferimento a un oggetto (ad esempio, String, Scanner, array), allora il riferimento stesso vive nello stack, mentre l’oggetto è nell’heap! Quando il riferimento scompare e non ci sono altri riferimenti all’oggetto, il garbage collector può eliminare l’oggetto.
Illustrazione
Stack (per main):
| int a = 42 |
| args |
-------------------
Heap:
| [oggetti creati tramite new] |
2. Perdite di memoria in Java: mito o realtà?
Molti principianti pensano: «In Java c’è il garbage collector! Quindi non possono esserci perdite di memoria!» Purtroppo è un mito che si dissolve al primo progetto di grandi dimensioni.
Il garbage collector elimina solo gli oggetti per i quali non esiste alcun riferimento vivo. Se da qualche parte è rimasto un riferimento (anche nel posto più inaspettato), l’oggetto resterà in memoria fino alla fine. O fino a OutOfMemoryError.
Esempio 1: collezione statica-trappola
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
// Ah, questa collezione statica!
private static final List<String> BIG_LIST = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
BIG_LIST.add("Riga numero " + i);
}
System.out.println("Aggiunto un milione di righe");
// Anche se main termina, BIG_LIST rimarrà in memoria finché la JVM è in esecuzione
}
}
Cosa succede?
- La variabile statica BIG_LIST vive finché il classe è caricata (di solito fino al termine della JVM).
- Tutte le stringhe aggiunte alla lista non possono essere rimosse dal garbage collector: esiste sempre un riferimento a esse tramite BIG_LIST.
- Se ti dimentichi di svuotare tali collezioni, otterrai una perdita di memoria.
Esempio 2: listener non rimossi (listeners)
import java.util.ArrayList;
import java.util.List;
class EventSource {
private final List<Runnable> listeners = new ArrayList<>();
public void addListener(Runnable listener) {
listeners.add(listener);
}
// ... altri metodi ...
}
public class ListenerLeakDemo {
public static void main(String[] args) {
EventSource source = new EventSource();
Runnable listener = () -> System.out.println("Evento!");
source.addListener(listener);
// Se si dimentica di chiamare source.removeListener(listener), il listener resterà in memoria per sempre!
}
}
Problema: se il listener non serve più ma non viene rimosso dalla lista, lui e tutti gli oggetti a cui fa riferimento rimarranno in memoria.
Esempio 3: cache che non si svuota mai
import java.util.HashMap;
import java.util.Map;
public class CacheLeakDemo {
private static final Map<String, byte[]> CACHE = new HashMap<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
// Ogni volta creiamo un array da 1 KB
CACHE.put("key" + i, new byte[1024]);
}
System.out.println("Aggiunto un milione di elementi alla cache");
// La cache cresce, la memoria si esaurisce, OutOfMemoryError!
}
}
Conclusione: anche con il GC è facile ottenere una perdita di memoria se non si gestisce correttamente il ciclo di vita degli oggetti!
3. Riferimenti deboli (WeakReference) e i loro amici
A volte abbiamo bisogno di cache o collezioni in cui gli oggetti possano essere rimossi dal garbage collector se non ci sono più altri riferimenti ad essi. Per questo sono stati inventati i riferimenti deboli (WeakReference).
Riferimenti normali (strong)
String s = new String("hello"); // riferimento strong
L’oggetto s vivrà in memoria finché esiste almeno un riferimento strong.
Riferimento debole (WeakReference)
import java.lang.ref.WeakReference;
public class WeakRefDemo {
public static void main(String[] args) {
String strong = new String("Ciao, mondo!");
WeakReference<String> weak = new WeakReference<>(strong);
System.out.println("Prima della pulizia: " + weak.get()); // c'è un riferimento
strong = null; // rimuoviamo il riferimento strong
System.gc(); // chiediamo al GC di ripulire la memoria (non è garantito!)
// Dopo un po' weak.get() può diventare null
System.out.println("Dopo il GC: " + weak.get());
}
}
Come funziona?
- Finché esiste almeno un riferimento strong a un oggetto, il GC non lo elimina.
- Se rimangono solo riferimenti deboli, l’oggetto può essere rimosso alla successiva raccolta.
- Il metodo weak.get() restituisce l’oggetto se è ancora vivo, oppure null se è stato eliminato.
Dove si usano i riferimenti deboli?
L’uso principale è nelle cache, dove non è critico se un oggetto viene rimosso dalla memoria. Per esempio, se si memorizzano in cache immagini ma non si vuole che la cache occupi tutta la memoria.
Esempio: WeakHashMap
WeakHashMap è una collezione in cui le chiavi sono mantenute tramite riferimenti deboli. Se nessun altro fa riferimento a una chiave, la coppia corrispondente viene rimossa dalla mappa.
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Object, String> map = new WeakHashMap<>();
Object key = new Object();
map.put(key, "Valore");
System.out.println("Prima della pulizia: " + map);
key = null; // rimuoviamo il riferimento strong alla chiave
System.gc(); // chiediamo al GC di lavorare
// Dopo un po' la mappa diventerà vuota!
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Dopo il GC: " + map);
}
}
Attenzione: WeakHashMap funziona solo per le chiavi: i valori sono mantenuti con normali riferimenti strong.
Soft, Weak, Phantom: tutta la famiglia dei riferimenti
In Java ci sono quattro tipi di riferimenti (ordinati per «forza»):
| Tipo di riferimento | Quando l’oggetto viene rimosso dal GC? | Dove si usa? |
|---|---|---|
|
Solo quando non esiste alcun riferimento strong | Variabili normali, collezioni |
|
In caso di memoria insufficiente | Cache che è preferibile mantenere più a lungo |
|
Al successivo passaggio del GC, se non ci sono riferimenti strong | Cache, WeakHashMap, listener |
|
Dopo la finalizzazione, per tracciare la rimozione dell’oggetto | Compiti speciali, pulizia fuori dall’heap |
- SoftReference — l’oggetto viene eliminato in caso di memoria insufficiente (adatto per cache di immagini, ecc.).
- WeakReference — viene eliminato alla prima raccolta, se non ci sono altri riferimenti.
- PhantomReference — il tipo più «fantasma», serve per scenari complessi ed è raramente usato dai principianti.
4. Pratica: esempio di perdita di memoria e sua correzione
Esempio di perdita: lista statica
import java.util.ArrayList;
import java.util.List;
public class LeakExample {
private static final List<byte[]> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new byte[1024 * 1024]); // 1 MB
if (i % 10 == 0) System.out.println("Aggiunti " + i + " MB");
}
}
}
Cosa succederà?
Il programma consumerà rapidamente tutta la memoria disponibile e terminerà con OutOfMemoryError, perché la lista statica mantiene i riferimenti a tutti gli array creati.
Correzione: usare riferimenti deboli
Se non è critico che tutti gli oggetti siano sempre disponibili, è possibile conservarli tramite riferimenti deboli:
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class LeakFixed {
private static final List<WeakReference<byte[]>> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new WeakReference<>(new byte[1024 * 1024]));
if (i % 10 == 0) System.out.println("Aggiunti " + i + " MB");
System.gc(); // Suggerimento per il GC (non garantisce la pulizia immediata!)
}
}
}
Ora gli array possono essere rimossi dal GC se non ci sono più riferimenti ad essi: la lista mantiene solo riferimenti deboli.
5. Scenari tipici di perdite di memoria
- Listener di eventi: ci si dimentica di rimuovere il listener — l’oggetto vive per sempre.
- Collezioni statiche: cache non svuotate, liste globali — tutto ciò può portare a perdite.
- Classi interne e lambda: se una classe interna o una lambda cattura un riferimento all’oggetto esterno, quest’ultimo non verrà eliminato finché vivrà l’oggetto esterno.
Esempio con classe interna
public class Outer {
private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 MB
public Runnable createTask() {
// La classe interna anonima cattura un riferimento a Outer!
return new Runnable() {
@Override
public void run() {
System.out.println("Attività in esecuzione");
}
};
}
public static void main(String[] args) {
Outer outer = new Outer();
Runnable task = outer.createTask();
// Anche se outer = null, task mantiene comunque un riferimento a bigArray!
}
}
Soluzione: usa classi interne statiche oppure sposta la logica in classi separate, per non mantenere riferimenti superflui.
6. Pratica: uso dei riferimenti deboli in una cache
Aggiungiamo a un’applicazione didattica una semplicissima cache che utilizza WeakHashMap.
import java.util.Map;
import java.util.WeakHashMap;
public class ImageCache {
private final Map<String, byte[]> cache = new WeakHashMap<>();
public void put(String name, byte[] data) {
cache.put(name, data);
}
public byte[] get(String name) {
return cache.get(name);
}
public static void main(String[] args) {
ImageCache cache = new ImageCache();
cache.put("cat", new byte[1024 * 1024]); // 1 MB
System.out.println("Gattino aggiunto alla cache");
// Se non ci sono più riferimenti alla chiave "cat", l'oggetto può essere rimosso dal GC
}
}
Nelle applicazioni reali (ad esempio nelle librerie di immagini) i riferimenti deboli aiutano a evitare l’esaurimento della memoria eliminando automaticamente i dati usati raramente.
7. Strong vs Weak: quando usare cosa?
- Riferimenti strong — predefiniti: usali per tutto ciò che deve vivere garantitamente.
- Riferimenti deboli — per cache, listener, quando non è critico se l’oggetto viene eliminato.
- Riferimenti soft — per cache che è preferibile mantenere più a lungo, ma eliminabili in caso di memoria insufficiente.
- Riferimenti phantom — per scenari avanzati (ad esempio, finalizzazione al di fuori dell’heap).
8. Errori tipici nella gestione della memoria e dei riferimenti
Errore n. 1: «Il GC farà tutto al posto mio!» Il garbage collector elimina solo gli oggetti per i quali non esiste alcun riferimento strong vivo. Se da qualche parte «ti sei dimenticato» un riferimento (per esempio in una collezione static), l’oggetto vivrà per sempre.
Errore n. 2: listener dimenticati. Hai aggiunto un listener a un oggetto ma non l’hai rimosso quando l’oggetto viene distrutto? Il listener, e tutto ciò che ha catturato, rimarrà in memoria.
Errore n. 3: cache senza riferimenti deboli. Usi un normale HashMap per una cache che dovrebbe svuotarsi automaticamente? Meglio usare WeakHashMap o SoftReference.
Errore n. 4: classi interne e lambda catturano l’oggetto esterno. Le classi interne (e le espressioni lambda) mantengono implicitamente un riferimento all’oggetto esterno. Se conservi istanze di tali classi più a lungo dell’oggetto esterno, avrai una perdita.
Errore n. 5: aspettarsi l’attivazione immediata del GC. La chiamata a System.gc() non garantisce che la raccolta avvenga subito. È solo una «richiesta» alla JVM, non un ordine.
GO TO FULL VERSION