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 Cleanmá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 elNative Heapy elDalvik Heapse 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
SwapPsspara 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
ActivitiesyViewRootImplfiltradas. Si sales de la app y fuerzas una GC, el recuento deActivitiesdeberí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 ononesi 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.
AndroidManifestes un<memory-budget>declarado en el manifiesto de la app,MemoryBudgetManageres un presupuesto que la app estableció en el tiempo de ejecución,MemoryLimiteres un límite de memoria del sistema yam memory-limiteres una anulación manual establecida conadb. - Uso máximo: Es la carga más alta desde que se creó el memcg (
memory.peak). Esto muestraN/Aen 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.
Inicia la app:
adb shell am start -W -n com.android.memorylab/.MainActivityVerifica la memoria de referencia:
adb shell dumpsys meminfo -s com.android.memorylab(La marca
-sproporciona un resumen breve y conciso de las categorías de memoria).Asignar memoria de Java: Presiona el botón Allocate Java Memory (10MB) 3 veces en la app.
Observa el cambio: Vuelve a ejecutar
meminfo -s. Deberías ver que el valor de Java Heap aumenta aproximadamente 30,000 KB (30 MB).Asignar memoria nativa: Presiona el botón Allocate Native Memory (1GB Incompressible) una vez.
Observa el cambio: Vuelve a ejecutar
meminfo -s. El valor de Native Heap ahora debería mostrar un aumento masivo (~1,000,000 KB).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-BUFes 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.anones la memoria anónima, como los montones de Java y nativos.filees la caché de páginas respaldada por archivos, como el código y los recursos.workingset_refault_anonyworkingset_refault_filecuentan 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.highcuenta cuántas veces se limitó el proceso por superar su límite dememory.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.
- Mmap Clean Memory: Presiona el botón Mmap Clean Anon Memory (10MB) en la app de MemoryLab una vez.
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.memorylabInspecciona con showmap:
# Replace <PID> with the actual process ID from the previous step adb shell showmap <PID> | grep "memorylab-clean"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 usamosmmappara reservar el espacio virtual, pero aún no escribimos en él (por lo que no se consume RAM física).Comparación del montón nativo: Contrasta esto con las asignaciones de
[anon:scudo:primary]que habrían aparecido enshowmapsi lo hubieras ejecutado después del paso 5 del ejercicio dememinfo. 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.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 enshowmapcomo[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 →