Perdita di memoria: cos'è, scenari tipici e diagnosi

Autore: IT Sectr Pubblicato: 2026-07-29 Tempo di lettura: 10 min

Perdita di memoria (memory leak) — situazione in cui un'applicazione non rilascia la memoria occupata da oggetti che non sono più necessari. Nello sviluppo mobile questo è particolarmente critico: l'heap limitato e l'assenza di swap portano a OutOfMemoryError e al crash dell'applicazione. Secondo Purdue University (2022), il 35% delle applicazioni Android su Google Play contiene almeno una perdita di memoria. Analizziamo scenari tipici, strumenti diagnostici e metodi di correzione.

Punti chiave

  • GC Root — punto di ingresso attraverso il quale il garbage collector determina gli oggetti vivi
  • Perdita di Context — passare Activity Context a un singleton trattiene l'intera gerarchia delle View
  • Handler con postDelayed — se l'Activity viene distrutta, l'Handler impedisce che venga raccolta dal GC
  • Heap dump — metodo principale di analisi delle perdite tramite MAT o Android Profiler
  • SoftReference — alternativa a WeakReference per cache con pulizia automatica in caso di memoria insufficiente

Cos'è una perdita di memoria nelle applicazioni mobili?

Perdita di memoria — situazione in cui la memoria allocata non viene restituita al sistema dopo che l'oggetto non è più necessario al programma. Il garbage collector considera tale oggetto come vivo perché esiste una catena di riferimenti attiva da un GC Root che punta ad esso.

In Java/Kotlin, il garbage collector funziona automaticamente, ma non può determinare che un oggetto è logicamente non necessario se esiste un riferimento tecnico ad esso. Lo sviluppatore deve interrompere esplicitamente le connessioni non necessarie. In Swift/Objective-C, ARC conta automaticamente i riferimenti, ma i retain cycle bloccano il decremento del contatore.

Il pericolo principale delle perdite è l'effetto cumulativo. Ogni perdita consuma una piccola quantità di memoria, ma con transizioni ripetute tra schermate (rotazioni dello schermo, apertura/chiusura di Activity), le perdite si accumulano fino a esaurire il limite dell'heap.

In cosa differisce una perdita dal gonfiore?

Perdita — l'oggetto è inaccessibile al codice ma non viene rimosso dal GC. Gonfiore — l'oggetto è logicamente necessario ma viene memorizzato in quantità eccessiva. Esempio di gonfiore: una cache di immagini di 100 MB con un set di lavoro di 30 MB. Entrambi i problemi portano a OOM, ma le cause e i metodi di trattamento sono diversi.

Come funziona il garbage collector e perché si verificano le perdite?

ART (Android Runtime) utilizza la garbage collection generazionale con compattazione concorrente. La memoria è divisa in generazione giovane (Young), generazione vecchia (Old) e oggetti grandi (Large). Gli oggetti che sopravvivono a diversi cicli di GC vengono spostati nella generazione Old, dove la raccolta avviene meno frequentemente — questo accelera i cicli normali.

Il GC inizia quando l'heap raggiunge una certa soglia di occupazione (di solito 75-85%). Durante il GC, tutti i thread dell'applicazione vengono sospesi (STW — Stop The World). Più oggetti vivi ci sono, più lunga è la pausa. Le perdite aumentano il numero di oggetti vivi, allungando le pause del GC.

Il collector determina gli oggetti vivi attraversando il grafo dalle GC Roots: campi statici, variabili di stack dei thread attivi, riferimenti JNI. Qualsiasi oggetto raggiungibile tramite riferimenti da queste radici è considerato vivo — anche se lo sviluppatore sa che non è più necessario.

kotlin
// Esempio: collezione statica come GC Root — perdita permanente
object GlobalHolder {
    val listeners = mutableListOf<WeakReference<Any>>()
}

class LeakingFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle ?= null) {
        super.onCreate(savedInstanceState)
        GlobalHolder.listeners.add(WeakReference(this))
        // WeakReference non impedisce il GC — comportamento corretto
    }
}

WeakReference risolve il problema: il GC ignora i riferimenti deboli nel determinare gli oggetti vivi. Se rimangono solo riferimenti deboli a un oggetto, verrà raccolto nel ciclo GC più vicino.

Scenari tipici di perdite in Android e iOS

Activity Context — lo scenario di perdita più massiccio in Android. Se un singleton, un campo statico o un servizio di lunga durata memorizza un riferimento a un Activity Context, l'intera Activity con tutte le sue View non può essere raccolta dal GC. Soluzione: utilizzare Application Context per gli oggetti di lunga durata.

Handler e messaggi inviati — Handler.postDelayed(runnable, delay) inserisce un messaggio nella coda del Main Looper. Se l'Activity viene distrutta prima della scadenza del ritardo, il messaggio è ancora nella coda e mantiene un riferimento tramite Runnable → classe anonima → classe esterna (Activity).

kotlin
class SafeActivity : AppCompatActivity() {
    private val mainHandler = Handler(Looper.getMainLooper())
    private val callback = Runnable { /* update UI */ }

    override fun onResume() {
        super.onResume()
        mainHandler.postDelayed(callback, 5000)
    }

    override fun onPause() {
        mainHandler.removeCallbacks(callback) // obbligatorio: pulire la coda
        super.onPause()
    }
}

Classi interne — una classe interna non statica ha un riferimento implicito all'istanza della classe esterna. Se la classe esterna è un'Activity e la classe interna viene passata all'esterno (ad esempio, a RecyclerView.Adapter), l'Activity non può essere raccolta.

  • TimerTask e ScheduledExecutorService — attività pianificate prima della distruzione dell'Activity
  • BroadcastReceiver — non deregistrato in onPause/onDestroy continua a mantenere il Context
  • ViewModel con riferimento a View — ViewModel sopravvive all'Activity, il riferimento alla View porta a una perdita
  • Retrofit Call — se Call non viene annullata, la risposta arriva a un Fragment distrutto

Strumenti diagnostici per le perdite di memoria

Android Studio Memory Profiler — strumento integrato per il monitoraggio dell'heap in tempo reale. Mostra un grafico della memoria utilizzata, il numero di allocazioni e oggetti per tipo. Consente di registrare un heap dump ed esportarlo in formato HPROF per l'analisi in MAT.

Eclipse MAT (Memory Analyzer Tool) — analizzatore desktop di heap dump. Costruisce automaticamente report Leak Suspects, evidenziando gli oggetti con il retained size maggiore e suggerendo la presunta catena GC Root per ogni oggetto sospetto.

Xcode Memory Graph Debugger — per iOS. Sospende l'applicazione e visualizza il grafo degli oggetti. I retain cycle sono evidenziati in rosso; è possibile cliccare su qualsiasi oggetto per vedere il suo retain count e i riferimenti.

StrumentoCapacitàComplessità
Memory ProfilerGrafico in tempo reale, heap dump, Object Allocation TrackingBassa
Eclipse MATDominator tree, Leak Suspects, query OQLMedia
LeakCanaryRilevamento automatico, traccia della perdita nella notificaMinima
Xcode Memory GraphGrafico visivo dei retain cycle, elenco oggetti viviBassa

Secondo l'Uber Engineering Blog, l'integrazione della profilazione automatica della memoria (LeakCanary + analisi heap dump) nella pipeline CI/CD riduce gli incidenti legati alla memoria in produzione del 60% entro 3 mesi.

Metodi per eliminare le perdite

Sostituire Context — se un oggetto sopravvive all'Activity, utilizzare applicationContext. Tutti gli oggetti di lunga durata (singleton, repository, helper di database) dovrebbero ricevere Application Context, non Activity Context. Eccezione: componenti UI che necessitano di accesso al tema o alle risorse specifiche dell'Activity.

Componenti lifecycle-aware — l'uso di LifecycleObserver, DefaultLifecycleObserver o estensioni reattive annulla automaticamente le sottoscrizioni in onDestroy. Android Jetpack fornisce lifecycleScope e viewModelScope, che vengono puliti dall'evento del ciclo di vita corrispondente.

Classe interna statica — se la classe interna non ha bisogno di accedere ai campi della classe esterna, renderla static. Una classe interna statica non ha un riferimento implicito alla classe esterna. Se l'accesso è necessario, utilizzare WeakReference per il riferimento esplicito.

kotlin
class MyActivity : AppCompatActivity() {

    // ❌ Classe interna non statica — riferimento implicito a MyActivity
    inner class BadListener : SomeListener {
        override fun onEvent() { /*...*/ }
    }

    // ✅ Classe interna statica — nessun riferimento implicito
    class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
        override fun onEvent() { /*...*/ }
    }
}

In iOS, utilizzare le liste di cattura: [weak self] nelle closure che possono sopravvivere al loro creatore. Per i delegati, utilizzare riferimenti deboli (weak var delegate). Per le closure che sono garantite essere chiamate solo durante la vita di self, è possibile utilizzare [unowned self], ma con cautela — l'accesso a un oggetto deallocato causerà un crash.

Domande frequenti

Come trovare una perdita senza strumenti speciali?

In Android, eseguire diverse transizioni tra schermate (Activity A → B → A → B) e verificare adb shell dumpsys meminfo package_name. Se il Total PSS cresce costantemente e non torna al valore originale — c'è una perdita. In iOS, analogamente: utilizzare Debug Memory Graph in Xcode per l'ispezione visiva.

Una coroutine Kotlin può causare una perdita?

Sì, se il CoroutineScope non viene annullato alla distruzione del componente. Una coroutine avviata in GlobalScope continua a eseguire anche dopo finish() dell'Activity. Soluzione: utilizzare viewModelScope (annullato in onCleared) o lifecycleScope (annullato in onDestroy). Per scope personalizzati, creare scope lifecycle-aware tramite LifecycleOwner.

Come influisce Bitmap sulle perdite?

Bitmap memorizza i dati dei pixel nell'heap nativo, non nell'heap Java. Ciò significa che il GC Java non vede la dimensione reale del Bitmap. Se Bitmap non viene riciclato con recycle() o il riferimento non viene annullato, la memoria nativa non viene liberata. Utilizzare BitmapFactory con inSampleSize per caricare copie ridotte e Glide/Coil per la gestione automatica della cache.

Cos'è una perdita attraverso un campo statico?

Un campo statico è un GC Root. Vive finché la classe è caricata (in Android — finché il Process è vivo). Se un campo statico referenzia un'Activity, Bitmap, View o qualsiasi altro oggetto pesante, quell'oggetto non verrà mai raccolto dal GC. Un campo statico è un riferimento eterno. Soluzione: memorizzare solo WeakReference o annullare il campo statico in onDestroy.

Come evitare perdite in iOS con ARC?

ARC rilascia automaticamente gli oggetti quando il contatore dei riferimenti forti scende a zero. Un retain cycle è l'unico modo per avere una perdita con ARC. Utilizzare sempre weak per i riferimenti genitore→figlio dove il figlio può sopravvivere al genitore (delegati, data source). Per le closure, utilizzare la lista di cattura [weak self] e verificare che self non sia nil all'interno della closure.

Riepilogo

  • Perdita di memoria — un oggetto è inaccessibile al codice ma non viene rimosso dal GC perché esiste un riferimento attivo da un GC Root
  • GC Roots includono campi statici, variabili di stack e riferimenti JNI; qualsiasi oggetto raggiungibile da essi è vivo
  • Perdita di Context — il problema più diffuso in Android: passare Activity Context a un singleton o campo statico
  • Handler e classe interna — la seconda causa più frequente: messaggi non annullati nella coda del Looper mantengono un riferimento all'Activity
  • LeakCanary — lo strumento standard di rilevamento automatico; prende un heap dump e mostra la catena GC Root esatta
  • lifecycleScope e viewModelScope risolvono il problema delle perdite tramite coroutine — annullamento automatico alla distruzione
  • Profilare la memoria in CI/CD: LeakCanary in debug + analisi heap dump nei test dovrebbero bloccare il merge in caso di nuove perdite

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.

Discuti il progetto

Leggi anche