Join us on the livestream at Android Dev Summit on 7-8 November 2018, starting at 10AM PDT!

Ver asignaciones de montones de Java y de memoria con Memory Profiler

Memory Profiler es un componente de Android Profiler que te ayuda a identificar fugas y migraciones de memoria que puedan generar interrupciones, congelamiento e incluso bloqueo en la app. En él se muestra un gráfico en tiempo real con el uso de memoria de tu app, y te permite capturar un volcado de montón, forzar la recolección de elementos no usados y realizar el seguimiento de asignaciones de memoria.

Para abrir Memory Profiler, sigue estos pasos:

  1. Haz clic en View > Tool Windows > Android Profiler (también puedes hacer clic en Android Profiler en la barra de herramientas).
  2. Selecciona el dispositivo y el proceso de la app del cual desees generar un perfil en la barra de herramientas del generador de perfiles de Android. Si conectaste un dispositivo mediante USB, pero no lo ves en la lista, asegúrate de haber habilitado la depuración USB.
  3. Haz clic en cualquier parte de la línea de tiempo **MEMORY ** para abrir Memory Profiler.

Como alternativa, puedes inspeccionar la memoria de tu app desde la línea de comandos con dumpsys y también ver eventos de recolección de elementos no usados en logcat.

Por qué debes generar perfiles para la memoria de tu app

Android proporciona un entorno de memoria administrada. Cuando determina que tu app ya no usa algunos objetos, el recolector de elementos no usados libera la memoria no usada para devolverla al montón. La forma en la que Android busca memoria sin usar se somete a optimizaciones constantes. Sin embargo, en algún punto en todas las versiones de Android, el sistema debe pausar brevemente tu código. La mayoría de las veces, las pausas son imperceptibles. Sin embargo, si tu app asigna memoria a una velocidad que supera la que el sistema es capaz de alcanzar para recolectarla, tu app podría experimentar una demora mientras el recolector libera suficiente memoria para satisfacer tus asignaciones. La demora podría hacer que tu app omitiera fotogramas y causara una lentitud visible.

Aun cuando tu app no muestre lentitud, si tiene fugas de memoria puede retenerla incluso mientras se encuentre en segundo plano. Este comportamiento puede desacelerar el rendimiento del resto de la memoria del sistema al forzar eventos innecesarios de recolección de elementos no usados. Eventualmente, el sistema se ve forzado a finalizar el proceso de tu app para recuperar la memoria. Luego, cuando el usuario regresa a tu app, esta debe reiniciarse por completo.

A fin de evitar estos problemas, debes usar Memory Profiler para lo siguiente:

  • Buscar patrones de asignación de memoria no deseados en la línea de tiempo que puedan estar causando problemas de rendimiento.
  • Volcar el montón de Java para ver los objetos que consumen memoria en un momento determinado. Varios volcados de montón a lo largo de un período prolongado pueden ayudar a identificar fugas de memoria.
  • Registrar asignaciones de memoria durante interacciones normales e intensivas del usuario para determinar con exactitud si tu código asigna demasiados objetos en poco tiempo o si asigna objetos que se fugan.

Para obtener más información sobre prácticas de programación que pueden reducir el uso de memoria de tu app, consulta Administra la memoria de tu app.

Información general sobre Memory Profiler

Cuando abras Memory Profiler por primera vez, verás una línea de tiempo detallada del uso de memoria de tu app y herramientas de acceso para forzar la recolección de elementos no usados, capturar un volcado de montón y registrar asignaciones de memoria.

Figura 1: Memory Profiler.

Como se indica en la figura 1, en la vista predeterminada para Memory Profiler se incluye lo siguiente:

  1. Un botón para forzar un evento de recolección de elementos no usados.
  2. Un botón para capturar un vaciado de montón.
  3. Un botón para registrar asignaciones de memoria. Este botón aparece únicamente cuando se conecta a un dispositivo con Android 7.1 o anterior.
  4. Botones para acercar y alejar la vista de la línea de tiempo.
  5. Un botón para ir directo a los datos de memoria en tiempo real.
  6. La línea de tiempo de eventos, en la que se muestran los estados de actividad, eventos de entrada del usuario y eventos de rotación de pantalla.
  7. La línea de tiempo de uso de memoria, que incluye lo siguiente:
    • Un gráfico apilado del volumen de memoria que se usa en cada categoría de memoria, como se indica con el eje Y a la izquierda, y la clave de color, en la parte superior.
    • Una línea punteada indica la cantidad de objetos asignados, como se indica con el eje Y a la derecha.
    • Un ícono para cada evento de recolección de elementos no usados.

Sin embargo, si usas un dispositivo con Android 7.1 o versiones anteriores, no se verá toda la información de generación de perfiles de manera predeterminada. Si ves el mensaje con la leyenda “Advanced profiling is unavailable for the selected process” (la generación de perfiles avanzada no se encuentra disponible para el proceso seleccionado), debes habilitar la generación de perfiles avanzada para ver lo siguiente:

  • Línea de tiempo de eventos
  • Cantidad de objetos asignados
  • Eventos de recolección de elementos no usados

En Android 8.0 y versiones posteriores, la generación de perfiles avanzada siempre se encuentra habilitada para apps depurables.

Cómo se recuenta la memoria

Los números que ves en la parte superior de Memory Profiler (figura 2) se basan en todas las páginas privadas de memoria que tu app confirma, según el sistema de Android. Este recuento no incluye páginas compartidas con el sistema ni otras apps.

Figura 2: Leyenda del recuento de memoria en la parte superior de Memory Profiler.

Las categorías en el recuento de memoria son las siguientes:

  • Java: memoria de objetos asignados desde código Java o Kotlin
  • Native: memoria de objetos asignados desde código C o C++.

    Aunque no uses C++ en tu app, puedes ver memoria nativa usada aquí porque el marco de trabajo de Android usa esta memoria para gestionar varias tareas por ti; por ejemplo, cuando maneja asset de imágenes y otros gráficos (aunque tu código esté en Java o Kotlin).

  • Graphics: memoria usada para colas de búfer de gráficos con el propósito de mostrar píxeles en la pantalla, incluidas las superficies y texturas GL, entre otras opciones. (Ten en cuenta que se trata de memoria compartida con la CPU y no de memoria dedicada de la GPU.)

  • Stack: memoria usada tanto por las pilas nativas como las de Java en tu app. Esto normalmente se relaciona con la cantidad de subprocesos en ejecución en tu app.

  • Code: memoria que tu app usa para código y recursos, como código de bytes dex, código dex optimizado o compilado, bibliotecas .so y fuentes.

  • Other: memoria usada por tu app que el sistema no determina cómo categorizar.

  • Allocated: cantidad de objetos Java y Kotlin asignados por tu app. No se tienen en cuenta los objetos asignados en C o C++.

    Cuando se conecta a un dispositivo con Android 7.1 o versiones anteriores, este recuento de asignación comienza solo en el momento en el cual Memory Profiler se conectó a tu app en ejecución. Por lo tanto, los objetos asignados antes de que inicies la generación de perfiles no se tienen en cuenta. Sin embargo, en Android 8.0 se incluye una herramienta de generación de perfiles en el dispositivo que realiza un seguimiento de todas las asignaciones, por lo que este número siempre representa la cantidad total de objetos Java pendientes en tu app en Android 8.0 y versiones posteriores.

Cuando se compara con los recuentos de memoria de la herramienta anterior, Android Monitor, el nuevo Memory Profiler registra tu memoria de manera diferente, por lo cual podría parecer que tu uso de memoria ahora fuera más elevado. Memory Profiler supervisa algunas categorías adicionales que aumentan el total, pero si solo te importa la memoria del montón de Java el número de “Java” debería ser similar al valor en la herramienta anterior.

Además, aunque el número de Java probablemente no sea exactamente igual al que viste en Android Monitor, el número nuevo contempla todas las páginas de memoria física asignadas al montón de Java de tu app desde que se bifurcó desde Zygote. Por lo tanto, esto proporciona una representación precisa del volumen de memoria física que realmente usa tu app.

Nota: Actualmente, Memory Profiler también muestra algunos casos falsos positivos de uso de memoria nativa en tu app que en realidad pertenece a las herramientas de generación de perfiles. Se agregan hasta 10 MB de memoria por cada 100 mil objetos aproximadamente. En una versión futura de las herramientas, estos números se excluirán de tus datos.

Ver asignaciones de memoria

Las asignaciones de memoria te muestran la manera en que se asignó cada objeto en tu memoria. Específicamente, Memory Profiler puede mostrarte lo siguiente sobre la asignación de objetos:

  • Qué tipos se asignaron y cuánto espacio ocupan.
  • El seguimiento de pila de cada asignación, incluidos los subprocesos.
  • El momento en que se desasignaron (solo cuando se usa un dispositivo con Android 8.0 o versiones posteriores).

Si tu dispositivo ejecuta Android 8.0 o versiones posteriores, puedes ver tus asignaciones de objetos en cualquier momento de la siguiente manera: Simplemente haz clic sobre la línea de tiempo, mantén presionado el botón y arrástrala para seleccionar la región en la que deseas ver las asignaciones (como se muestra en el video 1). No hay necesidad de comenzar una sesión de registro, ya que Android 8.0 y las versiones posteriores incluyen una herramienta de generación de perfiles en el dispositivo que constantemente realiza un seguimiento de las asignaciones de tu app.

Video 1: Con Android 8.0 y versiones posteriores, selecciona un área de la línea de tiempo existente para ver las asignaciones de objetos.

Si tu dispositivo tiene Android 7.1 o una versión anterior, haz clic en Record memory allocations en la barra de herramientas de Memory Profiler. Durante el registro, Android Monitor realiza el seguimiento de todas las asignaciones que ocurren en tu app. Cuando termines, haz clic en Stop recording (el mismo botón, mira el video 2) para ver las asignaciones.

Video 2: Con Android 7.1 y versiones anteriores, debes registrar las asignaciones de memoria de manera explícita.

Cuando selecciones una región de la línea de tiempo (o cuando termines una sesión de registro con un dispositivo con Android 7.1 o una versión anterior), la lista de objetos asignados aparece debajo de la línea de tiempo, y los objetos se muestran agrupados por nombre de clase y ordenados por recuento de montón.

Nota: En Android 7.1 y versiones anteriores, puedes registrar 65535 asignaciones como máximo. Si tu sesión de registro excede este límite, únicamente las 65535 asignaciones más recientes se guardan en el registro. (No existe un límite práctico en Android 8.0 y versiones posteriores).

Para inspeccionar el registro de asignaciones, sigue estos pasos:

  1. Explora la lista para encontrar objetos que tengan recuentos de montón inusualmente elevados y podrían experimentar fugas. Para que te resulte más sencillo encontrar clases conocidas, haz clic en el encabezado de la columna Class Name a fin de ordenarlas alfabéticamente. Luego haz clic en el nombre de una clase. El subpanel Instance View aparecerá a la derecha y en él se mostrará cada instancia de la clase, como puede verse en la figura 3.
  2. En el subpanel Instance View, haz clic en una instancia. La pestaña Call Stack aparecerá debajo y en ella se mostrará el punto en que se asignó esa instancia y el subproceso implicado.
  3. En la pestaña Call Stack, haz clic en cualquier línea para acceder al código en cuestión en el editor.

Figura 3: Aparecen detalles de cada objeto asignado en el subpanel Instance View, a la derecha.

De forma predeterminada, la lista de asignaciones a la izquierda se ordena según el nombre de la clase. En la parte superior de la lista, puedes usar el menú desplegable a la derecha para alternar los siguientes tipos de orden:

  • Arrange by class: agrupa a todas las asignaciones por nombre de clase.
  • Arrange by package: agrupa a todas las asignaciones por nombre de paquete.
  • Arrange by callstack: agrupa a todas las asignaciones en sus pilas de llamadas correspondientes.

Capturar un volcado de montón

Un volcado de montón muestra los objetos de tu app que consumen memoria en el momento en que lo capturas. En particular después de una sesión de usuario extensa, un volcado de montón puede ayudar a identificar fugas de memoria mostrando objetos que todavía están en ella y que , según tu criterio, ya no deberían estar ahí. Cuando captures un volcado de montón, podrás ver lo siguiente:

  • los tipos de objetos que asignó tu app y la cantidad de cada uno;
  • el volumen de memoria que usa cada objeto;
  • los puntos en que existen referencias a cada objeto en tu código;
  • la pila de llamadas para el punto en que se asignó un objeto. (Actualmente, las pilas de llamadas se encuentran disponibles con un volcado de montón solo en el caso de Android 7.1 y las versiones anteriores cuando capturas el volcado mientras registras las asignaciones).

Figura 4: Vista del volcado de montón.

Para capturar un volcado de montón, haz clic en Dump Java heap en la barra de herramientas de Memory Profiler. Mientras se vuelca el montón, el volumen de memoria Java puede aumentar temporalmente. Esto es normal, ya que el volcado de montón ocurre en el mismo proceso que tu app y requiere memoria para recolectar los datos.

El volcado de montón aparece debajo de la línea de tiempo de memoria y se muestran todos los tipos de clases que contiene, como se muestra en la figura 4.

Nota: Si necesitas ser más preciso respecto del momento en que se crea el volcado, puedes crear un volcado de montón en el punto crítico del código de tu app llamando a dumpHprofData().

Para inspeccionar tu volcado, sigue estos pasos:

  1. Explora la lista para encontrar objetos que tengan recuentos de montón inusualmente elevados y podrían experimentar fugas. Para que te resulte más sencillo encontrar clases conocidas, haz clic en el encabezado de la columna Class Name a fin de ordenarlas alfabéticamente. Luego haz clic en el nombre de una clase. El subpanel Instance View aparecerá a la derecha y en él se mostrará cada instancia de la clase, como puede verse en la figura 5.
  2. En el subpanel Instance View, haz clic en una instancia. La pestaña References aparecerá debajo y en ella se mostrarán todas las referencias al objeto en cuestión.

    También puedes hacer clic en la flecha junto al nombre de la instancia, para ver todos sus campos, y luego en el nombre de un campo para ver todas sus referencias. A su vez, si deseas ver información de la instancia para un campo, haz clic con el botón secundario en él y selecciona Go to Instance.

  3. En la pestaña References, si identificas una referencia que podría tener una fuga de memoria, haz clic con el botón secundario en ella y selecciona Go to Instance. Con esto, se selecciona la instancia correspondiente del volcado de montón y se muestran sus propios datos de instancia.

De forma predeterminada, el volcado de montón no te muestra el seguimiento de pila para cada objeto asignado. Para obtener el seguimiento de pila, debes comenzar a registrar asignaciones de memoria antes de hacer clic en Dump Java heap. Luego, puedes seleccionar una instancia en el subpanel Instance View y ver la pestaña Call Stack junto a la pestaña References, como se muestra en la figura 5. Sin embargo, es posible que algunos objetos se asignen antes de que comiences a registrar las asignaciones, por lo que la pila de llamadas no estará disponible para dichos objetos. Las instancias que sí incluyen una pila de llamadas se indican con la insignia “stack” en el ícono . (Lamentablemente, ya que el seguimiento de pila requiere que registres las asignaciones, actualmente no puedes ver el seguimiento de pila para volcados de montón en Android 8.0).

En el volcado de montón, busca fugas de memoria ocasionadas por alguno de los siguientes elementos:

  • Referencias duraderas a Activity, Context, View, Drawable y otros objetos que pueden hacer referencia al contenedor Activity o Context.
  • Clases internas no estáticas, como Runnable, que pueden contener una instancia Activity.
  • Cachés que almacenan objetos más tiempo del necesario.

Figura 5: La duración necesaria para capturar un volcado de montón se indica en la línea de tiempo.

En la lista de clases, puedes ver la siguiente información:

  • Heap Count: cantidad de instancias en el montón.
  • Shallow Size: tamaño total de todas las instancias en este montón (en bytes).
  • Retained Size: volumen total de memoria que se retiene a causa de todas las instancias de esta clase (en bytes).

En la parte superior de la lista de clases, puedes usar la lista desplegable de la izquierda para alternar los siguientes volcados de montón:

  • Default heap: cuando el sistema no especifica ningún montón.
  • App heap: el montón principal en el que tu app asigna memoria.
  • Image heap: imagen de inicio del sistema, con clases que se cargan previamente durante el tiempo de inicio. Se garantiza que estas asignaciones nunca se moverán o desaparecerán.
  • Zygote heap: montón de copia en escritura desde el cual se bifurca el proceso de una app en el sistema Android.

De forma predeterminada, la lista de objetos del montón se ordena por nombre de clase. Puedes usar el otro menú desplegable para alternar los siguientes tipos de orden:

  • Arrange by class: agrupa a todas las asignaciones por nombre de clase.
  • Arrange by package: agrupa a todas las asignaciones por nombre de paquete.
  • Arrange by callstack: agrupa a todas las asignaciones en su pila de llamadas correspondiente. Esta función solo funciona si capturas el volcado de montón mientras se registran las asignaciones. De todas formas, es posible que haya objetos en el montón que se asignen antes de que inicies el registro, por lo que esas asignaciones aparecen primero, simplemente enumeradas por nombre de clase.

De forma predeterminada, la lista se ordena mediante la columna Retained Size. Puedes hacer clic en cualquiera de los encabezados de las columnas para cambiar la manera en que se ordena la lista.

En la vista Instance View, cada instancia incluye lo siguiente:

  • Depth: el menor número de saltos desde cualquier raíz de recolección de elementos no usados (GC) a la instancia seleccionada.
  • Shallow Size: tamaño de la instancia.
  • Retained Size: volumen de memoria que domina esta instancia (de acuerdo con el árbol de dominadores.

Guardar el volcado de montón como HPROF

Cuando capturas un volcado de montón, los datos pueden verse en Memory Profiler únicamente mientras este se encuentre en ejecución. Cuando sales de la sesión de generación de perfiles, se pierde el vaciado de montón. Por lo tanto, si deseas guardarlo para revisarlo más tarde, exporta el vaciado de montón a un archivo HPROF haciendo clic en Export heap dump as HPROF file , en la barra de herramientas debajo de la línea de tiempo. En el diálogo que aparece, asegúrate de guardar el archivo con el sufijo .hprof.

Luego, puedes reabrir el archivo en Android Studio arrastrándolo y soltándolo en una ventana vacía del editor (o en la barra de la pestaña de archivos).

Para usar un analizador de HPROF como jhat, debes convertir el archivo HPROF del formato de Android al formato SE HPROF de Java. Puedes hacerlo con la herramienta hprof-conv provista en el directorio android_sdk/platform-tools/. Ejecuta el comando hprof-conv con dos argumentos: el archivo HPROF original y la ubicación para escribir el archivo HPROF convertido. Por ejemplo:

hprof-conv heap-original.hprof heap-converted.hprof

Técnicas para generar perfiles de tu memoria

Al usar Memory Profiler, debes forzar el código de tu app e intentar que se produzcan fugas de memoria. Una forma de provocar fugas de memoria en tu app es permitir que esta se ejecute durante un rato antes de inspeccionar el montón. Es posible que las fugas se desplacen hasta la parte superior de las asignaciones en el montón. Sin embargo, cuanto más pequeña sea la fuga, durante más tiempo deberás ejecutar la app para poder verla.

También puedes activar una fuga de memoria de una de las siguientes maneras:

  • Gira el dispositivo de la posición vertical a la horizontal y repite ese movimiento varias veces en diferentes estados de actividad. A menudo, la rotación del dispositivo puede hacer que en una app se produzca la fuga de un objeto Activity, Context o View debido a que el sistema recrea la Activity, y si tu app conserva una referencia a uno de esos objetos en otro lugar, el sistema no puede incluirlo en la recolección de elementos no usados.
  • Alterna tu app y otra en diferentes estados de actividad (navega hasta la pantalla principal y luego regresa a tu app).

Sugerencia: También puedes realizar los pasos anteriores usando el marco de trabajo de prueba monkeyrunner.