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
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.
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.
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>.
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
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.
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.
// 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) }
}
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.
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.
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.
Cuatro herramientas cubren la búsqueda de fugas desde la detección automática hasta el análisis profundo de Heap Dump.
| Herramienta | Método | Formato de resultados |
|---|---|---|
| LeakCanary | Monitorización automática | Heap Dump + stack trace de la fuga |
| Android Memory Profiler | Monitorización manual | Gráfico de memoria + Heap Dump |
| MAT (Eclipse) | Análisis profundo | Informe Dominator Tree + ruta GC Root |
| Perfetto | Traza a nivel de sistema | Lí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.
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.
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.
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.
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.
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.
// LeakCanary en pruebas
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // fallar si hay una fuga
}
}
Preguntas frecuentes
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.
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.
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.
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.
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
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.
Lea también