Una perdita di memoria (Memory Leak) è una situazione in cui l’applicazione mantiene riferimenti a oggetti che non sono più necessari, impedendo al garbage collector di liberare la memoria occupata. Secondo LeakCanary, anche nelle applicazioni ben scritte si riscontrano 3–5 perdite ogni 10.000 righe di codice. Ogni perdita riduce gradualmente la memoria disponibile, portando a rallentamenti e OutOfMemoryError.
Punti chiave
Una perdita di memoria (Memory Leak) è una situazione in cui un oggetto rimane raggiungibile attraverso una catena di riferimenti forti (Strong Reference), anche se logicamente non è più necessario all’applicazione. Il garbage collector (GC) considera tale oggetto come vivo e non libera la memoria che occupa. Di conseguenza, la memoria heap disponibile diminuisce costantemente e la frequenza delle pause del GC aumenta.
A differenza dei linguaggi con gestione manuale della memoria (C, C++), in Java/Kotlin una perdita non è un free() dimenticato, ma un riferimento dimenticato. Finché esiste un riferimento forte da una GC Root all’oggetto fuoriuscito, il GC lo considera necessario. GC Root tipiche: campi statici, thread attivi, stack di chiamate, riferimenti globali JNI.
Il pericolo delle perdite è il loro effetto cumulativo. Una perdita di 100 KB è impercettibile, ma 100 perdite di questo tipo occupano 10 MB e l’applicazione inizia a rallentare a causa dei GC frequenti. Una massa critica di perdite porta a OutOfMemoryError e all’arresto dell’applicazione. Sintomi di una perdita: crescita costante del consumo di memoria nel grafico del Profiler, pause frequenti del GC con STW (Stop The World) e degrado delle prestazioni dell’interfaccia utente.
Cinque tipi di perdite coprono il 95% dei casi nello sviluppo mobile. Ognuno ha la propria causa e un pattern di codice caratteristico.
La più nota perdita in Android è l’archiviazione di un riferimento statico a un’Activity o un Context. Codice tipico: un campo statico Activity che non viene annullato in onDestroy(). Finché il campo statico vive, l’intera Activity vive con il suo albero View, che può occupare 1–10 MB. Questa è la perdita classica che LeakCanary trova per prima.
Soluzione: non memorizzare mai Activity o Context in campi statici. Utilizzare Application Context per i singleton che sopravvivono all’Activity. Se è necessario un riferimento a un’Activity, utilizzare WeakReference<Activity>.
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
Le classi anonime e le classi interne non statiche mantengono implicitamente un riferimento alla classe contenitore. Un Runnable passato a un Handler che viene eseguito dopo onDestroy() trattiene l’intera Activity. Un callback Retrofit che cattura un’Activity fa lo stesso. Questo è il tipo di perdita più insidioso — il riferimento implicito non è visibile nel codice.
Le espressioni object e le lambda in Kotlin catturano anch’esse riferimenti alla classe esterna. Rendere le classi interne statiche (o di livello superiore in Kotlin) e passare i riferimenti esterni tramite WeakReference. Per le lambda, utilizzare un approccio Lifecycle-aware con viewLifecycleOwner.
Sottoscriversi ai servizi di sistema senza rimuovere la sottoscrizione è una perdita diretta. SensorManager, LocationManager, NotificationListener registrati in onResume() senza chiamare unregister in onPause() trattengono l’Activity. Analogamente: un Disposable RxJava non aggiunto a CompositeDisposable e una coroutine lanciata tramite GlobalScope.
Utilizzare componenti Lifecycle-aware: observe() con LifecycleOwner si rimuove automaticamente alla onDestroy(). Per RxJava — viewLifecycleOwner.lifecycle.addObserver con DisposableObserver. Per le coroutine — lifecycleScope.launch() è legato al ciclo di vita.
// rimozione automatica tramite Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// coroutine con lifecycleScope
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Un Bitmap occupa una quantità significativa di memoria heap: un bitmap FullHD è 1920 × 1080 × 4 byte = 8,3 MB. Se viene creato un Bitmap per ogni elemento dell’elenco e non viene chiamato recycle() durante la scomparsa, la memoria si esaurisce rapidamente. Nelle vecchie versioni di Android (prima della 3.0), il Bitmap era memorizzato nella memoria nativa, ma nelle versioni moderne si trova nell’heap Dalvik/ART e il GC può liberarlo solo se non c’è un riferimento forte.
Utilizzare Glide o Coil per caricare le immagini — queste librerie gestiscono automaticamente la memorizzazione nella cache e il riciclaggio. Se si lavora direttamente con Bitmap, chiamare bitmap.recycle() per le immagini grandi che non vengono più visualizzate e utilizzare inSampleSize per caricare copie ridotte.
Un Fragment ha due cicli di vita: quello del Fragment stesso e quello della sua View. Dopo onDestroyView(), l’albero della View viene distrutto, ma il Fragment stesso può rimanere in memoria se c’è un riferimento esterno. Un errore tipico è memorizzare un riferimento a un Fragment in un adattatore ViewPager o in un grafo di navigazione che non viene cancellato alla distruzione.
Non memorizzare mai un riferimento a un Fragment nei campi di oggetti a lunga vita. Utilizzare childFragmentManager per i fragment annidati e observe() con LifecycleOwner per il trasferimento di dati tra di essi. ViewPager2 ha risolto questo problema a livello API: FragmentTransactionAdapter gestisce correttamente il ciclo di vita.
Il rilevamento di una perdita richiede la verifica di due fatti: la memoria non ritorna dopo la durata di vita prevista e il numero di oggetti di un certo tipo cresce senza diminuire. Il processo diagnostico comprende tre fasi.
La prima fase è un controllo visivo tramite Memory Profiler in Android Studio. Aprire la scheda Memory, eseguire l’azione target (aprire e chiudere lo schermo), premere GC (Garbage Collection) e verificare se la memoria torna al livello iniziale. Se dopo 3–4 cicli di apertura-chiusura la memoria cresce costantemente — c’è una perdita.
La seconda fase è l’acquisizione di un Heap Dump. In Memory Profiler, premere Dump Java Heap. Aprire il file .hprof risultante in Android Studio: si vedranno tutti gli oggetti nell’heap con dimensioni e riferimenti. Cercare le classi il cui conteggio dovrebbe essere zero dopo la chiusura dello schermo. Ad esempio, MainActivity con un conteggio di 2 dopo la chiusura è un’ovvia perdita.
La terza fase è l’analisi di Retained Size e GC Root. In Android Studio, analizzare Retained Size: quanta memoria verrà liberata se si rimuove questo oggetto. Il percorso dalla GC Root all’oggetto mostra cosa lo trattiene: Static field → HashMap → Activity — e si vede il punto di perdita. Il pannello Reference mostra tutti i detentori dell’oggetto.
Quattro strumenti coprono la ricerca di perdite dal rilevamento automatico all’analisi approfondita dell’Heap Dump.
| Strumento | Metodo | Formato dei risultati |
|---|---|---|
| LeakCanary | Monitoraggio automatico | Heap Dump + stack trace della perdita |
| Android Memory Profiler | Monitoraggio manuale | Grafico memoria + Heap Dump |
| MAT (Eclipse) | Analisi approfondita | Report Dominator Tree + percorso GC Root |
| Perfetto | Tracciamento a livello di sistema | Linea temporale + memoria nativa |
LeakCanary è un must per qualsiasi progetto Android. Rileva automaticamente le perdite al termine del ciclo di vita di Activity/Fragment e mostra la posizione esatta della perdita con stack trace. Integrazione: una riga in build.gradle. LeakCanary 2.x non richiede inizializzazione manuale — registra automaticamente l’Application Watcher.
La prevenzione delle perdite è integrata nel processo di sviluppo attraverso una serie di regole e strumenti che controllano il codice in ogni fase.
Non memorizzare mai un riferimento a un’Activity, un Fragment o una View in un campo statico, singleton o oggetto a lunga vita. Se il riferimento è inevitabile, utilizzare WeakReference o memorizzare i dati tramite ViewModel, che vive esattamente quanto necessario e non trattiene direttamente una View.
ViewModel e LiveData di Android Architecture Components risolvono il problema del ciclo di vita a livello architetturale. ViewModel sopravvive alla rotazione dello schermo e non contiene riferimenti a View. LiveData rimuove automaticamente l’observer alla onDestroy(). Utilizzarli invece della sottoscrizione manuale ai servizi di sistema.
Durante la revisione del codice, prestare attenzione a: campi statici con tipi Context/View, classi anonime, lambda che catturano Activity, sottoscrizioni manuali, RxJava Disposable senza composite, memorizzazione di Fragment tramite Bundle. In Kotlin, controllare inoltre le coroutine con launch senza legame al ciclo di vita.
LeakCanary può funzionare come parte della pipeline di test: eseguire test di accettazione con LeakCanary e far fallire la build se viene trovata una perdita. Questo impedisce che le perdite raggiungano la produzione. Completare il controllo con la regola StaticFieldLeak di Android Lint — trova potenziali perdite a livello di analisi statica.
// LeakCanary nei test
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // fallire se c’è una perdita
}
}
Domande frequenti
La perdita è la causa, e OutOfMemoryError è la conseguenza. Una singola perdita non porta a OOM, ma l’accumulo di decine di perdite esaurisce l’Heap. OOM è un’eccezione fatale, mentre una perdita è un pattern che vi conduce nel tempo.
Attraverso Android Memory Profiler: aprire e chiudere lo schermo 5 volte, dopo ogni chiusura invocare GC. Se la memoria non torna al livello base — c’è una perdita. Acquisire un Heap Dump e trovare nell’elenco la classe Activity il cui conteggio è maggiore di 0 dopo la chiusura.
Parzialmente. Kotlin risolve il problema di null-safety ma non gestisce i riferimenti forti. Le coroutine con lifecycleScope e viewModelScope prevengono le perdite da attività in background, mentre sealed class e data class riducono il numero di stati che portano a perdite. La protezione principale sono i pattern architetturali, non le caratteristiche del linguaggio.
LeakCanary a volte dà falsi positivi: un oggetto può essere temporaneamente trattenuto dal sistema (ad esempio, InputMethodManager trattiene l’ultima View). Verificare manualmente: se Retained Size < 1 KB e la GC Root è un servizio di sistema, probabilmente è un falso positivo.
No. Le perdite sono possibili su qualsiasi piattaforma con GC: iOS (Swift/Objective-C), Flutter (Dart), browser web (JavaScript). I meccanismi sono gli stessi — riferimento forte da una GC Root. Su iOS, ARC gestisce automaticamente la memoria, ma i retain cycle tra oggetti creano la stessa perdita.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche