Fuga de memoria en aplicaciones móviles — qué es, causas y métodos de detección

Autor: IT Sectr Publicado: 2026-03-29 Tiempo de lectura: 9 min

Una fuga de memoria (Memory Leak) es una situación en la que la aplicación mantiene referencias a objetos que ya no son necesarios, impidiendo que el recolector de basura libere la memoria ocupada. Según LeakCanary, incluso en aplicaciones bien escritas aparecen entre 3 y 5 fugas por cada 10 000 líneas de código. Cada fuga reduce gradualmente la memoria disponible, provocando ralentizaciones y OutOfMemoryError.

Puntos clave

  • Memory Leak — un objeto permanece en memoria aunque no haya referencias activas a él desde la lógica de la aplicación
  • Referencias estáticas a Activity o Context — la causa más común de fugas en Android
  • LeakCanary — la herramienta estándar para la detección automática de fugas en Android
  • WeakReference y Application Context — técnicas básicas para prevenir fugas
  • Componentes Lifecycle-aware eliminan toda una clase de fugas relacionadas con suscripciones

Qué es una fuga de memoria

Una fuga de memoria (Memory Leak) es una situación en la que un objeto permanece accesible a través de una cadena de referencias fuertes (Strong Reference), aunque lógicamente ya no sea necesario para la aplicación. El recolector de basura (GC) considera dicho objeto como vivo y no libera la memoria que ocupa. Como resultado, la memoria disponible del montón (Heap) disminuye constantemente y la frecuencia de las pausas del GC aumenta.

A diferencia de los lenguajes con gestión manual de memoria (C, C++), en Java/Kotlin una fuga no es un free() olvidado, sino una referencia olvidada. Mientras exista una strong reference desde un GC Root hasta el objeto fugado, el GC lo considera necesario. GC Roots típicos: campos estáticos, hilos activos, pila de llamadas, referencias globales JNI.

El peligro de las fugas es su efecto acumulativo. Una fuga de 100 KB no se nota, pero 100 fugas de ese tipo ocupan 10 MB y la aplicación comienza a ralentizarse debido a los GC frecuentes. Una masa crítica de fugas provoca OutOfMemoryError y el cierre de la aplicación. Síntomas de una fuga: crecimiento constante del consumo de memoria en el gráfico del Profiler, pausas frecuentes del GC con STW (Stop The World) y degradación del rendimiento de la UI.

Tipos comunes de fugas en aplicaciones móviles

Cinco tipos de fugas cubren el 95% de los casos en el desarrollo móvil. Cada una tiene su propia causa y un patrón de código característico.

Referencia estática a Activity o Context

La más conocida de las fugas en Android es almacenar una referencia estática a una Activity o Context. Código típico: un campo estático Activity que no se anula en onDestroy(). Mientras viva el campo estático, vive toda la Activity con su árbol de View, que puede ocupar entre 1 y 10 MB. Esta es la fuga clásica que LeakCanary encuentra en primer lugar.

Solución: nunca almacene Activity o Context en campos estáticos. Use Application Context para singletons que sobrevivan a la Activity. Si necesita hacer referencia a una Activity, use WeakReference<Activity>.

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

Clases internas con referencia implícita

Las clases anónimas y las clases internas no estáticas mantienen implícitamente una referencia a la clase contenedora. Un Runnable pasado a un Handler que se ejecuta después de onDestroy() retiene toda la Activity. Una callback de Retrofit que captura una Activity hace lo mismo. Este es el tipo de fuga más insidioso: la referencia implícita no es visible en el código.

Las expresiones object y las lambdas de Kotlin también capturan referencias a la clase externa. Convierta las clases internas en estáticas (o de nivel superior en Kotlin) y pase las referencias externas a través de WeakReference. Para las lambdas, utilice un enfoque Lifecycle-aware con viewLifecycleOwner.

Suscriptores y suscripciones sin cancelar

Suscribirse a servicios del sistema sin cancelar la suscripción es una fuga directa. SensorManager, LocationManager, NotificationListener registrados en onResume() sin llamar a unregister en onPause() retienen la Activity. Del mismo modo: un Disposable de RxJava no añadido a CompositeDisposable y una corrutina lanzada a través de GlobalScope.

Utilice componentes Lifecycle-aware: observe() con LifecycleOwner se da de baja automáticamente en onDestroy(). Para RxJava — viewLifecycleOwner.lifecycle.addObserver con DisposableObserver. Para corrutinas — lifecycleScope.launch() está vinculado al ciclo de vida.

kotlin
// cancelación automática de suscripción via Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// corrutinas con lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap sin recycle

Un Bitmap ocupa una cantidad significativa de memoria del montón: un bitmap FullHD son 1920 × 1080 × 4 bytes = 8.3 MB. Si se crea un Bitmap para cada elemento de la lista y no se llama a recycle() al ocultarlo, la memoria se agota rápidamente. En versiones antiguas de Android (anteriores a 3.0), el Bitmap se almacenaba en memoria nativa, pero en las modernas está en el montón de Dalvik/ART, y el GC solo puede liberarlo si no hay una strong reference.

Utilice Glide o Coil para cargar imágenes: estas bibliotecas gestionan el almacenamiento en caché y el reciclaje automáticamente. Si trabaja directamente con Bitmap, llame a bitmap.recycle() para imágenes grandes que ya no se muestren y use inSampleSize para cargar copias reducidas.

Referencia a Fragment después de onDestroyView

Un Fragment tiene dos ciclos de vida: el del propio Fragment y el de su View. Después de onDestroyView(), el árbol de View se destruye, pero el Fragment en sí puede permanecer en memoria si hay una referencia externa. Un error típico es almacenar una referencia a un Fragment en un adaptador de ViewPager o en un gráfico de navegación que no se limpia al destruirse.

Nunca almacene una referencia a un Fragment en los campos de objetos de larga duración. Use childFragmentManager para fragments anidados y observe() con LifecycleOwner para la transferencia de datos entre ellos. ViewPager2 resolvió este problema a nivel de API: FragmentTransactionAdapter gestiona correctamente el ciclo de vida.

Cómo detectar una fuga de memoria

La detección de una fuga requiere verificar dos hechos: la memoria no vuelve después del tiempo de vida esperado y la cantidad de objetos de un tipo determinado crece sin disminuir. El proceso de diagnóstico incluye tres etapas.

La primera etapa es una comprobación visual a través del Memory Profiler en Android Studio. Abra la pestaña Memory, realice la acción deseada (abra y cierre la pantalla), pulse GC (Garbage Collection) y observe si la memoria vuelve al nivel inicial. Si después de 3–4 ciclos de apertura-cierre la memoria crece constantemente, hay una fuga.

La segunda etapa es tomar un Heap Dump. En el Memory Profiler, pulse Dump Java Heap. Abra el archivo .hprof resultante en Android Studio: verá todos los objetos en el montón con tamaños y referencias. Busque clases cuyo recuento debería ser cero después de cerrar la pantalla. Por ejemplo, MainActivity con un recuento de 2 después de cerrar es una fuga evidente.

La tercera etapa es analizar el Retained Size y el GC Root. En Android Studio, analice el Retained Size: cuánta memoria se liberará si elimina este objeto. La ruta desde el GC Root hasta el objeto muestra qué lo retiene: Static field → HashMap → Activity, y ve el punto de fuga. El panel Reference muestra todos los titulares del objeto.

Herramientas para la detección de fugas

Cuatro herramientas cubren la búsqueda de fugas desde la detección automática hasta el análisis profundo de Heap Dump.

HerramientaMétodoFormato de resultados
LeakCanaryMonitorización automáticaHeap Dump + stack trace de la fuga
Android Memory ProfilerMonitorización manualGráfico de memoria + Heap Dump
MAT (Eclipse)Análisis profundoInforme Dominator Tree + ruta GC Root
PerfettoTraza a nivel de sistemaLínea temporal + memoria nativa

LeakCanary es imprescindible para cualquier proyecto Android. Detecta automáticamente las fugas al final del ciclo de vida de Activity/Fragment y muestra la ubicación exacta de la fuga con un stack trace. Integración: una línea en build.gradle. LeakCanary 2.x no requiere inicialización manual: registra automáticamente el Application Watcher.

Cómo prevenir las fugas de memoria

La prevención de fugas se integra en el proceso de desarrollo mediante un conjunto de reglas y herramientas que verifican el código en cada etapa.

Regla de referencias fuertes

Nunca almacene una referencia a una Activity, Fragment o View en un campo estático, singleton u objeto de larga duración. Si no puede evitar la referencia, use WeakReference o almacene los datos a través de ViewModel, que vive exactamente el tiempo necesario y no retiene una View directamente.

Arquitectura Lifecycle-aware

ViewModel y LiveData de Android Architecture Components resuelven el problema del ciclo de vida a nivel de arquitectura. ViewModel sobrevive a la rotación de pantalla y no contiene referencias a View. LiveData da de baja automáticamente al observer en onDestroy(). Úselos en lugar de la suscripción manual a servicios del sistema.

Code Review centrado en GC Root

Durante la revisión de código, preste atención a: campos estáticos con tipos Context/View, clases anónimas, lambdas que capturan Activity, suscripciones manuales, RxJava disposable sin composite, almacenamiento de Fragment a través de Bundle. En Kotlin, verifique adicionalmente las corrutinas con launch sin vinculación al ciclo de vida.

Verificación automática en CI

LeakCanary puede funcionar como parte del pipeline de pruebas: ejecute pruebas de aceptación con LeakCanary y haga fallar la compilación si se encuentra una fuga. Esto evita que las fugas lleguen a producción. Complemente la verificación con la regla StaticFieldLeak de Android Lint: encuentra posibles fugas a nivel de análisis estático.

kotlin
// LeakCanary en pruebas
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // fallar si hay una fuga
    }
}

Preguntas frecuentes

¿En qué se diferencia una fuga de memoria de OutOfMemoryError?

Una fuga es la causa, y OutOfMemoryError es la consecuencia. Una fuga no provoca OOM, pero la acumulación de docenas de fugas agota el Heap. OOM es una excepción fatal, mientras que una fuga es un patrón que conduce a ella con el tiempo.

¿Cómo encontrar una fuga sin LeakCanary?

A través de Android Memory Profiler: abra y cierre la pantalla 5 veces, después de cada cierre invoque al GC. Si la memoria no vuelve al nivel base, hay una fuga. Tome un Heap Dump y busque en la lista la clase Activity cuyo recuento sea mayor que 0 después del cierre.

¿Puede Kotlin prevenir fugas a nivel de lenguaje?

Parcialmente. Kotlin resuelve el problema de null-safety, pero no gestiona las strong references. Las corrutinas con lifecycleScope y viewModelScope previenen fugas de tareas en segundo plano, mientras que sealed class y data class reducen la cantidad de estados que conducen a fugas. La protección principal son los patrones arquitectónicos, no las características del lenguaje.

¿Por qué LeakCanary encuentra una fuga que no existe?

LeakCanary a veces da falsos positivos: un objeto puede ser retenido temporalmente por el sistema (por ejemplo, InputMethodManager retiene la última View). Verifíquelo manualmente: si el Retained Size es < 1 KB y el GC Root es un servicio del sistema, probablemente sea un falso positivo.

¿Solo existen fugas de memoria en Android?

No. Las fugas son posibles en cualquier plataforma con GC: iOS (Swift/Objective-C), Flutter (Dart), navegadores web (JavaScript). Los mecanismos son los mismos: strong reference desde un GC Root. En iOS, ARC gestiona la memoria automáticamente, pero los retain cycles entre objetos crean la misma fuga.

Resumen

  • Memory Leak — un objeto que el GC no puede liberar debido a una strong reference olvidada
  • Referencias estáticas a Activity y Context — la causa más común de fugas
  • Referencias implícitas a través de clases anónimas, lambdas y suscripciones de RxJava son más insidiosas que las explícitas
  • LeakCanary encuentra fugas automáticamente y muestra el stack trace exacto
  • Componentes Lifecycle-aware (ViewModel, LiveData, lifecycleScope) eliminan una clase de fugas
  • Heap Dump y análisis de Retained Size — el método principal de diagnóstico manual
  • La prevención incluye code review centrado en strong references y verificación en CI con LeakCanary

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