Herramientas de evaluación rápida

Cuando necesites investigar el uso de memoria, comienza con herramientas de evaluación de alto nivel para identificar qué categorías de memoria son las más significativas.

dumpsys meminfo

La forma más común de obtener una descripción general del uso de memoria de una aplicación es con dumpsys meminfo. Para ver un desglose detallado que incluya todas las asignaciones de memoria, suele ser útil incluir la marca -a:

adb shell dumpsys meminfo -a <package_name_or_pid>

Este es un resultado de muestra de una aplicación con cierta presión de memoria (observa el valor alto de SwapPss):

** MEMINFO in pid 4562 [com.android.memorylab] **
                   Pss  Private  Private  SwapPss      Rss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty    Total     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------   ------
  Native Heap      394      268       64    17440     4264    29416    18128     8040
  Dalvik Heap      165        8        0      672     8196    40949    32757     8192
 Dalvik Other       47       28        0      604     1132
        Stack      112      108        4      312      120
...
        TOTAL    33197      580      788    21920   167776    70365    50885    16232

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:       12                          32608
         Native Heap:      268                           4264
                Code:      724                         125672
               Stack:      108                            120
            Graphics:        0                              0
       Private Other:      256
              System:    31829
             Unknown:                                    5112

           TOTAL PSS:    33197            TOTAL RSS:   167776       TOTAL SWAP PSS:    21920

 Objects
               Views:       15         ViewRootImpl:        1
         AppContexts:        5           Activities:        1
...

Interpreta los datos

  • Pss Total: Es el tamaño del conjunto proporcional. Es la suma de Private Dirty + Private Clean más el uso equitativo de la memoria de tu proceso compartida con otros procesos (como la imagen de arranque de Zygote). Esta es la mejor métrica para saber cuánta memoria usa esta app.
  • No sincronizada privada: Es la RAM que solo usa tu proceso y que se modificó (o es memoria anónima). Este es el número más importante para encontrar fugas, ya que esta memoria no se puede descartar.
  • No sincronizada privada: Es la RAM que solo usa tu proceso, pero es una copia sin modificar de un archivo en el almacenamiento (como un archivo DEX). El SO puede descartarlo si hay poca memoria.
  • SwapPss: Es la memoria que se comprimió y se intercambió en ZRAM. Esto puede suceder porque el sistema está bajo presión de memoria o porque el Android Runtime identificó procesos en segundo plano para compactar (mover sus páginas no utilizadas o inactivas a ZRAM). En el ejemplo de dumpsys, casi todo el Native Heap y el Dalvik Heap se movieron a ZRAM.

  • Punto clave: Los valores de Java Heap y Native Heap de la sección "App Summary" representan la memoria residente (física). Si la memoria de una aplicación se comprimió en ZRAM, estos valores de resumen específicos pueden parecer mucho más bajos de lo esperado. Siempre verifica el TOTAL SWAP PSS o la columna detallada SwapPss para obtener el panorama completo.

  • Resumen de la app: Es una categorización de nivel superior que combina las filas detalladas en buckets comprensibles.

  • Objetos: Es útil para hacer un seguimiento de las instancias de Activities y ViewRootImpl filtradas. Si sales de la app y fuerzas una GC, el recuento de Activities debería volver a 0.

Sección de presupuesto de memoria

En Android 18 y versiones posteriores, dumpsys meminfo también imprime una sección de presupuesto de memoria cuando vuelca una sola app. En esta sección, se leen las estadísticas de memcg del proceso y de toda la app (el paquete), por lo que no necesitas encontrar los archivos de cgroup por tu cuenta. Aquí se muestra la sección de la app de ejemplo de MemoryLab en primer plano, después de presionar tres veces Allocate Java Memory (10MB):

 Memory Budget
                        Process(KB)       Package(KB)
                        -----------       -----------
  Current Usage:              83820             87976
  Limit:                       none           4718592
  Limit Source:                none     MemoryLimiter
  Peak Usage:                 85136             89292
  Swap Used:                      0                 0

  Usage Breakdown
    Anonymous:                81700             81700
    File:                        96              4200
    File Mapped:                 16               616
    Shared Memory:               16                16
    Kernel:                    1580              1632
    Page Tables:                640               640
    Slab:                       439               458

  Pressure & Reclaim
    Throttle Events:              0                 0
    Refaults (Anon):              0                 0
    Refaults (File):              0                 0
  • Uso actual: Es la memoria que se cobra al memcg (memory.current). Este es el número que limitan los presupuestos de memoria. En la muestra, aumentó alrededor de 36,700 KB después de las tres asignaciones de Java de 10 MB, en línea con TOTAL PSS. El total del paquete es mayor que el total del proceso, incluso para una app de un solo proceso, cuando aún no se recuperaron las páginas de archivos almacenados en caché de una ejecución anterior.
  • Límite: Es el umbral de recuperación (memory.high) que se aplica en este momento o none si no hay uno. En el ejemplo, el paquete tiene un límite de memoria del sistema, y el proceso no tiene un límite propio.
  • Fuente del límite: Es el origen del límite. AndroidManifest es un <memory-budget> declarado en el manifiesto de la app, MemoryBudgetManager es un presupuesto que la app estableció en el tiempo de ejecución, MemoryLimiter es un límite de memoria del sistema y am memory-limiter es una anulación manual establecida con adb.
  • Uso máximo: Es la carga más alta desde que se creó el memcg (memory.peak). Esto muestra N/A en los kernels que no lo admiten.
  • Swap Used: Es la memoria comprimida en ZRAM (memory.swap.current).
  • Desglose de uso: Son los campos principales de memory.stat, como la memoria anónima y la respaldada por archivos.
  • Presión y recuperación: Eventos de limitación cuenta cuántas veces el memcg superó su límite de memory.high. Los errores de página repetidos cuentan las páginas que se intercambiaron o expulsaron y que, luego, se volvieron a necesitar. Si estos números aumentan mientras usas la app, el límite es más estricto que el conjunto de trabajo de la app.

Con la marca -a o -v, en la sección también se enumeran todos los contadores sin procesar de memory.stat y memory.events.

Ejercicio práctico: Clasificación rápida con meminfo

En este ejercicio, se muestra cómo aparecen los diferentes tipos de asignaciones en meminfo.

  1. Inicia la app:

    adb shell am start -W -n com.android.memorylab/.MainActivity
    
  2. Verifica la memoria de referencia:

    adb shell dumpsys meminfo -s com.android.memorylab
    

    (La marca -s proporciona un resumen breve y conciso de las categorías de memoria).

  3. Asignar memoria de Java: Presiona el botón Allocate Java Memory (10MB) 3 veces en la app.

  4. Observa el cambio: Vuelve a ejecutar meminfo -s. Deberías ver que el valor de Java Heap aumenta aproximadamente 30,000 KB (30 MB).

  5. Asignar memoria nativa: Presiona el botón Allocate Native Memory (1GB Incompressible) una vez.

  6. Observa el cambio: Vuelve a ejecutar meminfo -s. El valor de Native Heap ahora debería mostrar un aumento masivo (~1,000,000 KB).

  7. Compara con memcg (Android 18 y versiones posteriores): Consulta la sección Memory Budget al final del resultado de meminfo -s. El Uso actual crece junto con el PSS TOTAL después de cada asignación, ya que las asignaciones de Java y nativas se cobran al memcg de la app.

Vista global

Para ver el uso de memoria en todo el sistema, ejecuta lo siguiente:

adb shell dumpsys meminfo

Esto proporciona un resumen del uso de RAM, incluida la RAM total, la RAM libre y una lista de procesos ordenados por PSS.

Métricas clave del sistema

  • ZRAM: Es el espacio de intercambio comprimido en la RAM.
  • DMA-BUF: Gráficos y búferes de memoria compartida Las versiones modernas de Android usan DMA-BUF de forma extensiva para la renderización de la IU. Si el total de DMA-BUF es muy alto, investiga los procesos con muchos gráficos con /sys/kernel/debug/dma_buf/bufinfo.
  • RAM perdida: Es la memoria que no se tiene en cuenta en el kernel ni en ningún proceso (p.ej., regiones reservadas para el hardware).

Estadísticas de memcg

Cada proceso de la app tiene su propio grupo de control de memoria (memcg), y el kernel mantiene los totales de la memoria que se le asigna (consulta memcg para saber cómo difiere esta contabilización de RSS y PSS). La lectura de estos archivos es económica y no interrumpe tu app, por lo que puedes muestrearlos cada segundo mientras ejecutas un flujo de usuarios.

Busca la ruta de acceso de memcg de un proceso desde la línea 0:: de /proc/<PID>/cgroup en lugar de codificar la ruta de acceso de forma fija. Pasa cada comando del dispositivo a adb shell como una sola cadena entre comillas para que la shell del dispositivo vea las comillas:

PID=$(adb shell pidof -s <package_name>)

CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")

adb shell "cat $CG/memory.current $CG/memory.swap.current"

adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

Los archivos de claves son los siguientes:

  • memory.current: Es la memoria que se cobra al proceso, en bytes. Este es el número que limitan los presupuestos de memoria.
  • memory.swap.current: Es la memoria comprimida en ZRAM, en bytes.
  • memory.stat: Es un desglose del cargo, en bytes para los tamaños. anon es la memoria anónima, como los montones de Java y nativos. file es la caché de páginas respaldada por archivos, como el código y los recursos. workingset_refault_anon y workingset_refault_file cuentan las páginas que se intercambiaron o expulsaron y que luego se volvieron a necesitar. Los recuentos de fallas de página repetidas en aumento significan que el proceso tiene poca memoria.
  • memory.events: Es el recuento de eventos de límite de memoria. high cuenta cuántas veces se limitó el proceso por superar su límite de memory.high.

El directorio superior del memcg del proceso pertenece a toda la app (su UID). Sus archivos incluyen la memoria de todos los procesos en ejecución de la app, además de las páginas de archivos almacenadas en caché que quedaron de procesos anteriores de la app que se cerraron y aún no se recuperaron:

adb shell "cat $CG/../memory.current"

Una app puede consultar su propio uso de cgroup en proceso a través de MemoryBudgetManager. Si recibes un error de permiso cuando inspeccionas los archivos cgroup de una app desde adb shell, ejecuta adb root en una compilación userdebug o una imagen de emulador sin Google Play y, luego, vuelve a intentarlo.

Para obtener una explicación guiada de estos archivos con la app de ejemplo de MemoryLab, consulta Ejercicio práctico: Explora memcg.

showmap

Para obtener un desglose mucho más detallado de las asignaciones de memoria de un proceso, usa showmap. Esta herramienta lee desde /proc/<pid>/smaps.

adb shell showmap <pid>

showmap enumera cada mapa de memoria del proceso. Entre las asignaciones de claves que se deben buscar, se incluyen las siguientes:

  • [anon:scudo:primary] o [anon:libc_malloc]: Es el montón nativo. Las versiones modernas de Android usan el asignador Scudo.
  • *.art: Son imágenes del montón de Java (p.ej., imagen de arranque).
  • [anon:dalvik-main space]: Es el montón de Java principal.
  • *.so: Bibliotecas compartidas.
  • *.{oat,dex,odex,vdex}: Es el código DEX compilado.

Puedes ordenar el resultado para encontrar los consumidores de memoria privada más grandes:

# Sort by private clean + private dirty memory
adb shell showmap <pid> | awk '{print $6+$7, $0}' | sort -rn | head -n 10

Resultado de muestra:

 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
-------- -------- -------- -------- -------- -------- -------- -------- -------- --------- --------- --------- -------- -------- -------- ---- ------------------------------
2776    18176     5784     2823        0     3008      308     2468     4700     4700         0         0         0        0        0        0   47 [anon:scudo:primary]
560      952      876      657      316        0      560        0       12       12         0         0         0        0        0        0    8 /data/app/.../base.apk
448   393216      448      448        0        0        0      448        0        0         0         0         0        0        0        0    2 [anon:dalvik-main space]
...

Ejercicio práctico: Análisis detallado con showmap

Aprende a identificar asignaciones de memoria específicas y a diferenciar entre memoria limpia y sucia.

  1. Mmap Clean Memory: Presiona el botón Mmap Clean Anon Memory (10MB) en la app de MemoryLab una vez.
  2. Obtén el ID de proceso: La IU de la app muestra su PID. También puedes encontrarla con este comando:

    adb shell pidof com.android.memorylab
    
  3. Inspecciona con showmap:

    # Replace <PID> with the actual process ID from the previous step
    adb shell showmap <PID> | grep "memorylab-clean"
    
  4. Identifica la asignación: Deberías ver una asignación anónima etiquetada como [anon:memorylab-clean] con un tamaño virtual de exactamente 10,240 KB. Ten en cuenta que el valor de private dirty para este mapa probablemente sea 0. Esta es memoria anónima "limpia" porque usamos mmap para reservar el espacio virtual, pero aún no escribimos en él (por lo que no se consume RAM física).

  5. Comparación del montón nativo: Contrasta esto con las asignaciones de [anon:scudo:primary] que habrían aparecido en showmap si lo hubieras ejecutado después del paso 5 del ejercicio de meminfo. Debido a que esa asignación nativa de 1 GB escribe en cada página, esas páginas están "sucias" y se mostrarán en gran medida en la columna private dirty.

  6. Sugerencia avanzada (objetos grandes): Observa las asignaciones de montón de Java del ejercicio meminfo. Debido a que son arrays de bytes de 10 MB, es probable que aparezcan en showmap como [anon:dalvik-free list large object space]. Android Runtime (ART) coloca los objetos de más de 12 KB en un espacio de objetos grandes (LOS) separado para evitar la fragmentación en el montón principal.

procstats

Mientras que meminfo proporciona una instantánea en el tiempo, procstats proporciona datos históricos mediante el muestreo periódico del estado y el uso de memoria de todas las aplicaciones en ejecución. Esta es la herramienta principal que se usa para supervisar el estado de la memoria a largo plazo y es la que genera los informes de uso de memoria en la Configuración de Android.

# View stats for the last 3 hours
adb shell dumpsys procstats --hours 3

Información sobre los estados

El resultado de procstats puede ser abrumador. La parte más importante son las métricas entre paréntesis junto a cada estado del proceso:

Top: 100% (54MB-64MB-67MB / 49MB-58MB-61MB / 110MB-120MB-130MB over 4)

El formato es el siguiente: (% de tiempo en el estado) (minPss-avgPss-maxPss / minUss-avgUss-maxUss / minRss-avgRss-maxRss sobre muestras)

  • Pss (tamaño del conjunto proporcional): La memoria compartida se divide de forma proporcional entre los procesos.
  • Uss (tamaño del conjunto único): Es la memoria que solo usa este proceso (equivalente a Private Clean + Private Dirty).
  • Rss (tamaño del conjunto residente): Es la memoria física total que se usa, incluidas todas las páginas compartidas (tiende a contar de más).

Por qué es importante:

  • App estable: Los valores mínimo, promedio y máximo están cerca entre sí.
  • Fuga de memoria: El valor máximo es significativamente más alto que el mínimo, y el promedio aumenta con el tiempo a medida que se incrementa el recuento de muestras.
  • Uso con picos: El valor máximo es mucho más alto que el promedio, lo que indica operaciones pesadas transitorias (como el procesamiento de imágenes) que podrían causar OOM si ocurren en el momento equivocado.

Estados de los procesos

procstats hace un seguimiento de las apps en varios estados (a menudo abreviados):

  • Persist: Son procesos críticos del sistema que siempre se están ejecutando.
  • Superior: La app se encuentra actualmente en primer plano y es visible para el usuario.
  • ImpFg (primer plano importante): Son procesos en segundo plano críticos (como los IME) o apps con un servicio en primer plano.
  • ImpBg (segundo plano importante): Son los procesos en segundo plano que se consideran importantes para el sistema.
  • Fgs/BFgs: Servicios en primer plano o servicios en primer plano vinculados.
  • Service/ServRst: Son servicios en segundo plano o servicios que se están reiniciando.
  • Cached/LastAct: Son las apps que se almacenan en caché en segundo plano para un reinicio rápido. El sistema puede finalizar estos procesos de forma segura.

Estado de la memoria del dispositivo y estadísticas del tiempo de ejecución

procstats también hace un seguimiento de cuánto tiempo pasó el dispositivo completo bajo presión de memoria. Busca la sección Estadísticas de tiempo de ejecución:

Run time Stats:
  SOff/Norm: +1h10m15s
   SOn/Norm: +15m20s
        Low: +2m10s
       Crit: +45s
  • SOff/SOn: Pantalla apagada en comparación con pantalla encendida.
  • Norm/Mod/Low/Crit: Son los niveles de presión de memoria (Normal, Moderate, Low, Critical).

Si un dispositivo pasa mucho tiempo en los estados Low o Crit, se sentirá lento, ya que el kernel recupera la memoria de forma agresiva y el Low Memory Killer (LMK) comienza a finalizar los procesos en segundo plano. Luego, puedes consultar las estadísticas por paquete para ver qué apps tienen un uso de memoria alto, específicamente cuando el dispositivo se encuentra en estos estados de estrés.


← Conceptos | ↑ Arriba | Java →