Fuga de memoria: qué es, escenarios típicos y diagnóstico

Autor: IT Sectr Publicado: 2026-07-29 Tiempo de lectura: 10 min

Fuga de memoria (memory leak) — situación en la que una aplicación no libera la memoria ocupada por objetos que ya no son necesarios. En el desarrollo móvil esto es especialmente crítico: el heap limitado y la ausencia de swap provocan OutOfMemoryError y el cierre de la aplicación. Según Purdue University (2022), el 35% de las aplicaciones Android en Google Play contienen al menos una fuga de memoria. Analicemos escenarios típicos, herramientas de diagnóstico y métodos de corrección.

Puntos clave

  • GC Root — punto de entrada mediante el cual el recolector de basura determina objetos vivos
  • Fuga de Context — pasar Activity Context a un singleton retiene toda la jerarquía de View
  • Handler con postDelayed — si la Activity se destruye, el Handler impide que sea recolectada
  • Heap dump — método principal de análisis de fugas mediante MAT o Android Profiler
  • SoftReference — alternativa a WeakReference para cachés con limpieza automática ante falta de memoria

¿Qué es una fuga de memoria en aplicaciones móviles?

Fuga de memoria es una situación en la que la memoria asignada no se devuelve al sistema después de que el objeto ya no es necesario para el programa. El recolector de basura considera que dicho objeto está vivo porque existe una cadena de referencias activa desde un GC Root que apunta a él.

En Java/Kotlin, el recolector de basura funciona automáticamente, pero no puede determinar que un objeto es lógicamente innecesario si existe una referencia técnica hacia él. El desarrollador debe romper explícitamente las conexiones innecesarias. En Swift/Objective-C, ARC cuenta las referencias automáticamente, pero los retain cycles bloquean el decremento del contador.

El peligro principal de las fugas es el efecto acumulativo. Cada fuga consume una pequeña cantidad de memoria, pero con las transiciones repetidas entre pantallas (rotaciones de pantalla, apertura/cierre de Activities) las fugas se acumulan hasta agotar el límite del heap.

¿En qué se diferencia una fuga de la hinchazón?

Fuga — el objeto es inaccesible para el código pero no es eliminado por el GC. Hinchazón — el objeto es lógicamente necesario pero se almacena en cantidad excesiva. Ejemplo de hinchazón: una caché de imágenes de 100 MB con un conjunto de trabajo de 30 MB. Ambos problemas llevan a OOM, pero las causas y los métodos de tratamiento son diferentes.

¿Cómo funciona el recolector de basura y por qué ocurren las fugas?

ART (Android Runtime) utiliza recolección de basura generacional con compactación concurrente. La memoria se divide en generación joven (Young), generación vieja (Old) y objetos grandes (Large). Los objetos que sobreviven varios ciclos de GC se mueven a la generación Old, donde la recolección ocurre con menos frecuencia — esto acelera los ciclos normales.

El GC se inicia cuando el heap alcanza un cierto umbral de ocupación (normalmente 75-85%). Durante el GC, todos los hilos de la aplicación se pausan (STW — Stop The World). Cuantos más objetos vivos, más larga es la pausa. Las fugas aumentan la cantidad de objetos vivos, alargando las pausas del GC.

El recolector determina los objetos vivos recorriendo el grafo desde las GC Roots: campos estáticos, variables de pila de hilos activos, referencias JNI. Cualquier objeto alcanzable mediante referencias desde estas raíces se considera vivo — incluso si el desarrollador sabe que ya no es necesario.

kotlin
// Ejemplo: colección estática como GC Root — fuga 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 no impide el GC — comportamiento correcto
    }
}

WeakReference resuelve el problema: el GC ignora las referencias débiles al determinar objetos vivos. Si solo quedan referencias débiles a un objeto, será recolectado en el ciclo de GC más cercano.

Escenarios típicos de fugas en Android e iOS

Activity Context — el escenario de fuga más masivo en Android. Si un singleton, un campo estático o un servicio de larga duración almacena una referencia a un Activity Context, toda la Activity con todas sus Views no puede ser recolectada por el GC. Solución: use Application Context para objetos de larga duración.

Handler y mensajes enviados — Handler.postDelayed(runnable, delay) coloca un mensaje en la cola de Main Looper. Si la Activity se destruye antes de que expire el retraso, el mensaje sigue en la cola y mantiene una referencia a través de Runnable → clase anónima → clase externa (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) // obligatorio: limpiar cola
        super.onPause()
    }
}

Clases internas — una clase interna no estática tiene una referencia implícita a la instancia de la clase externa. Si la clase externa es una Activity y la clase interna se pasa a algún lugar externo (por ejemplo, a RecyclerView.Adapter), la Activity no puede ser recolectada.

  • TimerTask y ScheduledExecutorService — tareas programadas antes de la destrucción de la Activity
  • BroadcastReceiver — no registrado en onPause/onDestroy sigue manteniendo el Context
  • ViewModel con referencia a View — ViewModel sobrevive a la Activity, la referencia a la View provoca una fuga
  • Retrofit Call — si Call no se cancela, la respuesta llega a un Fragment destruido

Herramientas de diagnóstico de fugas de memoria

Android Studio Memory Profiler — herramienta integrada para monitorización del heap en tiempo real. Muestra un gráfico de memoria ocupada, cantidad de asignaciones y objetos por tipo. Permite grabar un heap dump y exportarlo en formato HPROF para su análisis en MAT.

Eclipse MAT (Memory Analyzer Tool) — analizador de escritorio de heap dumps. Construye automáticamente informes de Leak Suspects, que destacan los objetos con mayor retained size y sugieren la posible cadena de GC Root para cada objeto sospechoso.

Xcode Memory Graph Debugger — para iOS. Pausa la aplicación y visualiza el grafo de objetos. Los retain cycles se resaltan en rojo; se puede hacer clic en cualquier objeto para ver su retain count y referencias.

HerramientaCapacidadesComplejidad
Memory ProfilerGráfico en tiempo real, heap dump, Object Allocation TrackingBaja
Eclipse MATDominator tree, Leak Suspects, consultas OQLMedia
LeakCanaryDetección automática, trace de fuga en notificaciónMínima
Xcode Memory GraphGrafo visual de retain cycles, lista de objetos vivosBaja

Según el Uber Engineering Blog, la integración del perfilado automático de memoria (LeakCanary + análisis de heap dump) en el pipeline CI/CD reduce los incidentes relacionados con memoria en producción en un 60% en 3 meses.

Métodos para eliminar fugas

Reemplazar Context — si un objeto sobrevive a la Activity, use applicationContext. Todos los objetos de larga duración (singletons, repositorios, helpers de base de datos) deben recibir Application Context, no Activity Context. Excepción: componentes UI que necesitan acceso al tema o recursos específicos de la Activity.

Componentes conscientes del ciclo de vida — el uso de LifecycleObserver, DefaultLifecycleObserver o extensiones reactivas cancela automáticamente las suscripciones en onDestroy. Android Jetpack proporciona lifecycleScope y viewModelScope, que se limpian con el evento de ciclo de vida correspondiente.

Clase interna estática — si la clase interna no necesita acceso a los campos de la clase externa, hágalo static. Una clase interna estática no tiene referencia implícita a la clase externa. Si se necesita acceso, use WeakReference para la referencia explícita.

kotlin
class MyActivity : AppCompatActivity() {

    // ❌ Clase interna no estática — referencia implícita a MyActivity
    inner class BadListener : SomeListener {
        override fun onEvent() { /*...*/ }
    }

    // ✅ Clase interna estática — sin referencia implícita
    class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
        override fun onEvent() { /*...*/ }
    }
}

En iOS, use listas de captura: [weak self] en clausuras que puedan sobrevivir a su creador. Para delegados, use referencias débiles (weak var delegate). Para clausuras que se garantiza que solo se invocarán durante la vida de self, se puede usar [unowned self], pero con cuidado — acceder a un objeto desasignado provocará un crash.

Preguntas frecuentes

¿Cómo encontrar una fuga sin herramientas especiales?

En Android, realice varias transiciones entre pantallas (Activity A → B → A → B) y verifique adb shell dumpsys meminfo package_name. Si el Total PSS crece de forma estable y no vuelve al valor original — hay una fuga. En iOS, de forma similar: use Debug Memory Graph en Xcode para inspección visual.

¿Puede una corrutina de Kotlin causar una fuga?

Sí, si el CoroutineScope no se cancela al destruirse el componente. Una corrutina lanzada en GlobalScope continúa ejecutándose incluso después de finish() de la Activity. Solución: use viewModelScope (se cancela en onCleared) o lifecycleScope (se cancela en onDestroy). Para scopes personalizados, cree scopes conscientes del ciclo de vida mediante LifecycleOwner.

¿Cómo afecta Bitmap a las fugas?

Bitmap almacena datos de píxeles en el heap nativo, no en el heap de Java. Esto significa que el GC de Java no ve el tamaño real del Bitmap. Si no se llama a recycle() en el Bitmap o no se anula la referencia, la memoria nativa no se libera. Use BitmapFactory con inSampleSize para cargar copias reducidas y Glide/Coil para la gestión automática de caché.

¿Qué es una fuga a través de un campo estático?

Un campo estático es una GC Root. Vive mientras la clase esté cargada (en Android — mientras el Process esté vivo). Si un campo estático referencia una Activity, Bitmap, View o cualquier otro objeto pesado, ese objeto nunca será recolectado por el GC. Un campo estático es una referencia eterna. Solución: almacene solo WeakReference o anule el campo estático en onDestroy.

¿Cómo evitar fugas en iOS con ARC?

ARC libera objetos automáticamente cuando el contador de referencias fuertes llega a cero. Un retain cycle es la única forma de fuga con ARC. Use siempre weak para referencias padre→hijo donde el hijo pueda sobrevivir al padre (delegados, data sources). Para clausuras, use la lista de captura [weak self] y verifique que self no sea nil dentro de la clausura.

Resumen

  • Fuga de memoria — un objeto es inaccesible para el código pero no es eliminado por el GC porque hay una referencia activa desde una GC Root
  • GC Roots incluyen campos estáticos, variables de pila y referencias JNI; cualquier objeto alcanzable desde ellas está vivo
  • Fuga de Context — el problema más extendido en Android: pasar Activity Context a un singleton o campo estático
  • Handler y clase interna — la segunda causa más frecuente: mensajes no cancelados en la cola de Looper mantienen una referencia a la Activity
  • LeakCanary — la herramienta estándar de detección automática; toma un heap dump y muestra la cadena exacta de GC Root
  • lifecycleScope y viewModelScope resuelven el problema de fugas mediante corrutinas — cancelación automática al destruir
  • Perfile la memoria en CI/CD: LeakCanary en debug + análisis de heap dump en ejecuciones de prueba deben bloquear el merge ante nuevas fugas

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también