Cómo ver asignaciones de montón de Java y memoria con Memory Profiler

Memory Profiler es un componente de Android Profiler que te ayuda a identificar fugas y pérdidas de memoria que puedan generar inestabilidades, fallas e incluso bloqueos de apps. Muestra un gráfico en tiempo real del uso de memoria de tu app y te permite capturar un volcado de montón, forzar la recolección de elementos no utilizados 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 Profile  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 de Android Profiler. 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 del cronograma 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 utilizados 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 utilizados libera la memoria que no se usó 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 de 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 puede experimentar una demora mientras el recolector libera suficiente memoria para satisfacer tus asignaciones. La demora podría hacer que tu app omitiera marcos y causara una lentitud visible.

Aunque tu app no muestre lentitud, si tiene fugas de memoria, puede retener esa memoria incluso mientras se encuentre en segundo plano. Este comportamiento puede desacelerar el rendimiento del resto de la memoria del sistema forzando eventos innecesarios de recolección de elementos no utilizados. Finalmente, el sistema se ve forzado a finalizar el proceso de tu app para recuperar la memoria. Luego, cuando el usuario regresa a la 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 el cronograma 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 Cómo administrar la memoria de tu app.

Descripción general de Memory Profiler

Cuando abras Memory Profiler por primera vez, verás un cronograma detallado del uso de memoria de tu app y herramientas de acceso para forzar la recolección de elementos no utilizados, 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 de Memory Profiler, se incluye lo siguiente:

  1. Un botón para forzar un evento de recolección de elementos no utilizados
  2. Un botón para capturar un volcado de montón

    Nota: Solo aparecerá un botón para registrar las asignaciones de memoria a la derecha del botón de volcado del montón si estás conectado a un dispositivo con Android 7.1 (nivel de API 25) o una versión inferior.

  3. Un menú desplegable para especificar la frecuencia con la que el generador de perfiles captura las asignaciones de memoria (es posible que elegir la opción adecuada te ayude a mejorar el rendimiento de la app a la hora de generar el perfil)
  4. Botones para acercar y alejar la vista del cronograma
  5. Un botón para ir directo a los datos de memoria en tiempo real
  6. El cronograma de eventos, donde se muestran los estados de actividad, eventos de entrada del usuario y eventos de rotación de pantalla
  7. El cronograma de uso de memoria, que incluye lo siguiente:
    • Un gráfico apilado del volumen de memoria que se usa en cada categoría, como se indica con el eje Y a la izquierda, y la clave de color, en la parte superior
    • Una línea punteada que 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 utilizados

Sin embargo, si usas un dispositivo con Android 7.1 o una versión anterior, no se verá toda la información de generación de perfiles de manera predeterminada. Si aparece el mensaje "Advanced profiling is unavailable for the selected process", deberás habilitar la generación de perfiles avanzada para ver lo siguiente:

  • El cronograma de eventos
  • La cantidad de objetos asignados
  • Los eventos de recolección de elementos no utilizados

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

Cómo se registra 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 confirma tu app, según el sistema Android. Este registro 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 del recuento de memoria son las siguientes:

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

    Incluso si no usas C++ en tu app, puedes ver memoria nativa usada aquí porque el marco de trabajo de Android utiliza ese tipo de memoria para administrar varias tareas por ti; por ejemplo, cuando maneja elementos de imágenes y otros gráficos (aunque tu código esté en Java o Kotlin).

  • Gráficos: Es la 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.

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

  • Código: Es la 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.

  • Otras: Esta categoría incluye la memoria usada por tu app que el sistema no sabe cómo categorizar.

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

    Cuando se establece conexión con un dispositivo que ejecuta Android 7.1 o una versión anterior, este recuento de asignación comienza solamente en el momento en el que se conecta Memory Profiler a tu app en ejecución. Por lo tanto, no se tienen en cuenta los objetos asignados antes de que inicies la generación de perfiles. 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 que podría parecer que el uso ahora es 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 de la herramienta anterior. Además, aunque es probable que el número de Java no sea exactamente igual al que viste en Android Monitor, la cifra nueva contempla todas las páginas de memoria física asignadas al montón de Java de tu app desde que se bifurcó a partir de Zygote. Por lo tanto, esto proporciona una representación precisa del volumen de memoria física que realmente usa tu app.

Cómo ver asignaciones de memoria

Las asignaciones de memoria te muestran cómo se asignó cada objeto Java y referencia de JNI en la 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, incluido el subproceso
  • El momento en que se desasignaron (solo cuando se usa un dispositivo con Android 8.0 o una versión posterior)

Si tu dispositivo ejecuta Android 8.0 o una versión posterior, puedes ver las asignaciones de tus objetos en cualquier momento de la siguiente manera: arrastra el cronograma a fin de seleccionar la región para 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 realiza un seguimiento de las asignaciones de tu app de forma constante.

Video 1: En el caso de Android 8.0 y versiones posteriores, selecciona un área del cronograma existente para ver las asignaciones de objetos

Si tu dispositivo ejecuta 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, Memory Profiler rastrea todas las asignaciones que se producen en tu app. Cuando hayas terminado, haz clic en Stop recording (el mismo botón; mira el video 2) para ver las asignaciones.

Video 2: En el caso de Android 7.1 y versiones anteriores, debes registrar las asignaciones de memoria de forma explícita

Cuando selecciones una región del cronograma (o cuando termines una sesión de registro con un dispositivo que ejecute Android 7.1 o una versión anterior), aparecerá debajo la lista de objetos asignados, y se mostrarán los objetos agrupados por nombre de clase y ordenados por recuento de montón.

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 que 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 mostrarlas en orden alfabético. Luego, haz clic en el nombre de una clase. Aparecerá el panel Instance View a la derecha y, en él, se mostrará cada instancia de la clase, como puede verse en la figura 3.
    • De manera alternativa, puedes ubicar objetos rápidamente haciendo clic en Filter o presionando Ctrl + F (Cmd + F en Mac) e ingresando el nombre de una clase o paquete en el campo de búsqueda. También puedes buscar por nombre de método si seleccionas Arrange by callstack en el menú desplegable. Si deseas utilizar expresiones regulares, marca la casilla junto a Regex. También puedes marcar Match case si tu consulta de búsqueda distingue entre mayúsculas y minúsculas.
  2. En el panel Instance View, haz clic en una instancia. Aparecerá debajo la pestaña Call Stack y, en ella, se mostrará el punto en que se asignó esa instancia y el subproceso.
  3. En la pestaña Call Stack, haz clic con el botón derecho en cualquier línea y elige Jump to Source para abrir ese código en el editor.

Figura 3: Los detalles de cada objeto asignado aparecen en la opción Instance View ubicada a la derecha

Puedes usar los dos menús que aparecen encima de la lista de objetos asignados para elegir qué montón inspeccionar y cómo organizar los datos.

En el menú de la izquierda, elige qué montón inspeccionar:

  • default heap: El sistema no especifica un montón.
  • image heap: La imagen de inicio del sistema, que contiene clases precargadas durante ese período. Se garantiza que estas asignaciones nunca se moverán ni desaparecerán.
  • zygote heap: Es el montón de copia en escritura en el que se bifurca un proceso de la app en el sistema Android.
  • app heap: Es el montón principal en el que asigna memoria tu app.
  • JNI heap: Es el montón que muestra dónde se asignan y liberan las referencias de la interfaz nativa de Java (JNI).

En el menú ubicado a la derecha, elige cómo organizar las asignaciones:

  • Arrange by class: Agrupa todas las asignaciones según el nombre de la clase. Esta es la opción predeterminada.
  • Arrange by package: Agrupa todas las asignaciones según el nombre del paquete.
  • Arrange by callstack: Agrupa todas las asignaciones en su pila de llamadas correspondiente.

Cómo mejorar el rendimiento de la app durante la generación de perfiles

Para mejorar el rendimiento de tu app durante la generación de perfiles, la herramienta muestra, de forma predeterminada, las asignaciones de memoria periódicamente. Cuando realizas pruebas en dispositivos con nivel de API 26 o superior, puedes cambiar este comportamiento mediante el menú desplegable Allocation Tracking. Las opciones disponibles son las siguientes:

  • Full: Captura todas las asignaciones de objetos en la memoria. Este es el comportamiento predeterminado en Android Studio 3.2 y versiones anteriores. Si tu app asigna una gran cantidad de objetos, es posible que se produzcan ralentizaciones visibles en ella durante la generación de perfiles.
  • Sampled: Muestra las asignaciones de objetos en la memoria a intervalos regulares. Esta es la opción predeterminada y tiene menos impacto en el rendimiento de la app durante la generación de perfiles. Es posible que las apps que asignan una gran cantidad de objetos en un corto intervalo de tiempo muestren ralentizaciones visibles.
  • Off: Detiene el seguimiento de la asignación de memoria de tu app.

Cómo ver referencias globales de JNI

La interfaz nativa de Java (JNI) es un marco de trabajo que permite que se llamen entre sí el código Java y el código nativo.

Las referencias de JNI se administran manualmente mediante código nativo, por lo que es posible que los objetos Java que utilice el código nativo se mantengan activos durante demasiado tiempo. Algunos objetos del montón de Java pueden llegar a ser inalcanzables si se descarta una referencia de JNI sin primero borrarla de forma explícita. Además, es posible agotar el límite de referencias globales de JNI.

A fin de solucionar estos problemas, utiliza la vista JNI heap de Memory Profiler para examinar todas las referencias globales de JNI y filtrarlas por tipos de Java y pilas de llamadas nativas. Con esta información, puedes encontrar cuándo y dónde se crean y se borran las referencias globales de JNI.

Mientras se esté ejecutando la app, selecciona una parte del cronograma que quieras inspeccionar y elige JNI heap en el menú desplegable de la parte superior de la lista, como se muestra más abajo. Podrás inspeccionar los objetos del montón como lo harías normalmente y hacer doble clic en los objetos de la pestaña Allocation Call Stack para ver dónde se asignan y se publican las referencias de JNI en tu código, tal como se muestra en la figura 4.

Figura 4: Visualización de referencias globales de JNI

A fin de inspeccionar las asignaciones de memoria para el código JNI de tu app, debes implementar tu aplicación en un dispositivo que ejecute Android 8.0 o una versión posterior.

Para obtener más información sobre JNI, consulta estas sugerencias relacionadas.

Cómo 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 capturas el volcado. 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 de tu código
  • La pila de llamadas del punto en que se asignó un objeto (actualmente, las pilas de llamadas están disponibles con un volcado de montón solo en el caso de Android 7.1 y versiones anteriores cuando capturas el volcado mientras registras las asignaciones)

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, es posible que aumente de forma temporal el volumen de la memoria Java. Esto es normal, ya que el volcado de montón ocurre en el mismo proceso que tu app y requiere memoria para recolectar datos.

El volcado de montón aparece debajo del cronograma de memoria, y se muestran todos los tipos de clases que contiene, como puede apreciarse en la figura 5.

Figura 5: Visualización del volcado de montón

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

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

  • Allocations: Muestra la cantidad de asignaciones que contiene el montón.
  • Native Size: Es la cantidad total (en bytes) de memoria nativa que utiliza este tipo de objeto. Esta columna solo se ve en Android 7.0 y versiones posteriores.

    Aquí, verás la memoria de algunos objetos asignados en Java debido a que Android usa memoria nativa para algunas clases de marcos de trabajo, como Bitmap.

  • Shallow Size: Es la cantidad total (en bytes) de memoria Java que utiliza este tipo de objeto.

  • Retained Size: Es el tamaño total (en bytes) de la memoria retenida debido a todas las instancias de esta clase.

Puedes usar los dos menús que aparecen encima de la lista de objetos asignados para elegir qué volcados de montón inspeccionar y cómo organizar los datos.

En el menú de la izquierda, elige qué montón inspeccionar:

  • default heap: El sistema no especifica un montón.
  • app heap: Es el montón principal en el que asigna memoria tu app.
  • image heap: La imagen de inicio del sistema, que contiene clases precargadas durante ese período. Se garantiza que estas asignaciones nunca se moverán ni desaparecerán.
  • zygote heap: Es el montón de copia en escritura en el que se bifurca un proceso de la app en el sistema Android.

En el menú ubicado a la derecha, elige cómo organizar las asignaciones:

  • Arrange by class: Agrupa todas las asignaciones según el nombre de la clase. Esta es la opción predeterminada.
  • Arrange by package: Agrupa todas las asignaciones según el nombre del paquete.
  • Arrange by callstack: Agrupa todas las asignaciones en su pila de llamadas correspondiente. Solo podrás usar esta función 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 aparecerán primero, enumeradas por nombre de clase.

De forma predeterminada, la lista se ordena mediante la columna Retained Size. Si prefieres ordenarla por los valores de otra columna, haz clic en el encabezado correspondiente.

Haz clic en el nombre de una clase para abrir la ventana Instance View a la derecha (como se muestra en la figura 6). Cada instancia listada incluye lo siguiente:

  • Depth: Es el menor número de saltos desde cualquier raíz de recolección de elementos no utilizados a la instancia seleccionada.
  • Native Size: Es el volumen de esta instancia en la memoria nativa. Esta columna solo se ve en Android 7.0 y versiones posteriores.
  • Shallow Size: Volumen de esta instancia en la memoria Java.
  • Retained Size: Volumen de memoria que domina esta instancia (de acuerdo con el árbol de dominadores).

Figura 6: En la línea de tiempo, se indica la duración requerida para capturar un volcado de montón

Para inspeccionar tu volcado, sigue estos pasos:

  1. Explora la lista para encontrar objetos que tengan recuentos de montón inusualmente elevados y que 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 mostrarlas en orden alfabético. Luego, haz clic en el nombre de una clase. Aparecerá el panel Instance View a la derecha y, en él, se mostrará cada instancia de la clase, como puede verse en la figura 6.
    • De manera alternativa, puedes ubicar objetos rápidamente haciendo clic en Filter o presionando Ctrl + F (Cmd + F en Mac) e ingresando el nombre de una clase o paquete en el campo de búsqueda. También puedes buscar por nombre de método si seleccionas Arrange by callstack en el menú desplegable. Si deseas utilizar expresiones regulares, marca la casilla junto a Regex. También puedes marcar Match case si tu consulta de búsqueda distingue entre mayúsculas y minúsculas.
  2. En el panel Instance View, haz clic en una instancia. Aparecerá debajo la pestaña References 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. Asimismo, si deseas ver información de la instancia correspondiente a 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. De esta manera, se selecciona la instancia correspondiente del volcado de montón y se muestran sus propios datos de instancia.

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

  • Referencias duraderas a objetos Activity, Context, View, Drawable y otros que podrían contener una referencia a los contenedores Activity o Context
  • Clases internas no estáticas, como Runnable, que pueden contener una instancia de Activity
  • Cachés que almacenan objetos más tiempo del necesario

Cómo guardar un volcado de montón como un archivo HPROF

Cuando capturas un volcado de montón, los datos pueden verse en Memory Profiler únicamente mientras este se encuentra en ejecución. Cuando sales de la sesión de generación de perfiles, se pierde el volcado. Por lo tanto, si quieres guardarlo para revisarlo más tarde, expórtalo como un archivo HPROF. En Android Studio 3.1 y versiones anteriores, el botón Export capture to file se encuentra en el lado izquierdo de la barra de herramientas, debajo del cronograma. En Android Studio 3.2 y versiones posteriores, se incluye un botón Export Heap Dump a la derecha de cada entrada de Heap Dump, en el panel Sessions. En el cuadro de diálogo Export as que aparece, guarda el archivo con la extensión de nombre de archivo .hprof.

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 la versión convertida. Por ejemplo:

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

Cómo importar un archivo de volcado de montón

Para importar un archivo HPROF (.hprof), haz clic en Start a new profiling session en el panel Sessions, selecciona Load from file y elige el archivo desde el explorador.

También puedes importar un archivo HPROF arrastrándolo desde el explorador hasta una ventana del editor.

Cómo 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. Para ello, puedes permitir que se ejecute tu app 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 ocasionar la fuga de un objeto Activity, Context o View porque el sistema recrea Activity y, si tu app conserva una referencia a uno de esos objetos en algún otro lugar, el sistema no puede incluirlo en la recolección de elementos no utilizados.
  • 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.