Quando você precisar investigar o uso da memória, comece com ferramentas de avaliação de alto nível para identificar quais categorias de memória são mais significativas.
dumpsys meminfo
A maneira mais comum de ter uma visão geral do uso da memória de um aplicativo é
dumpsys meminfo. Para ver um detalhamento que inclui todos os mapeamentos de memória, costuma ser útil incluir a flag -a:
adb shell dumpsys meminfo -a <package_name_or_pid>
Confira um exemplo de saída de um aplicativo sob alguma pressão de memória (observe o alto 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ção dos dados
- Pss Total: o tamanho do conjunto proporcional. É a soma de
Private Dirty + Private Clean+ a parcela equitativa de memória do seu processo compartilhada com outros processos (como a imagem de inicialização do Zygote). Essa é a melhor métrica para "quanta memória esse app é responsável". - Particular e suja: é a RAM que apenas seu processo está usando e que foi modificada (ou é uma memória anônima). Esse é o número mais crítico para encontrar vazamentos, porque essa memória não pode ser descartada.
- Particular e limpa: RAM que apenas seu processo está usando, mas que é uma cópia não modificada de um arquivo no armazenamento (como um arquivo DEX). Ele pode ser descartado pelo SO se a memória estiver baixa.
SwapPss: memória que foi compactada e trocada para a ZRAM. Isso pode acontecer porque o sistema está sob pressão de memória ou porque o Android Runtime identificou processos em segundo plano para compactar (movendo as páginas não utilizadas ou inativas para a ZRAM). No exemplo de
dumpsys, quase todos osNative HeapeDalvik Heapforam movidos para a ZRAM.Importante: os valores de Java Heap e Native Heap na seção "App Summary" representam a memória residente (física). Se a memória de um aplicativo tiver sido compactada em ZRAM, esses valores de resumo específicos podem parecer muito menores do que o esperado. Sempre verifique o TOTAL SWAP PSS ou a coluna detalhada
SwapPsspara ter uma visão completa.Resumo do app: uma categorização de nível mais alto que combina as linhas detalhadas em buckets compreensíveis.
Objects: útil para rastrear instâncias vazadas de
ActivitieseViewRootImpl. Se você sair do app e forçar uma GC, a contagem deActivitiesvai voltar para 0.
Seção de orçamento de memória
No Android 18 e versões mais recentes, o dumpsys meminfo também mostra uma seção de orçamento de memória
ao despejar um único app. Essa seção lê as estatísticas do memcg
do processo e de todo o app (o pacote), para que
você não precise encontrar os arquivos do cgroup por conta própria. Esta é a seção do
app de exemplo MemoryLab em primeiro plano, depois de três toques em 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 atual: memória cobrada do memcg (
memory.current). Esse é o número que os orçamentos de memória limitam. No exemplo, ele aumentou em cerca de 36.700 KB após as três alocações de Java de 10 MB, de acordo com o TOTAL PSS. O total do pacote é maior que o total do processo, mesmo para um app de processo único, quando as páginas de arquivos em cache de uma execução anterior ainda não foram reivindicadas. - Limite: o limite de recuperação (
memory.high) que se aplica no momento ounone, se não houver um. No exemplo, o pacote tem um limite de memória do sistema, e o processo não tem um limite próprio. - Origem do limite: de onde vem o limite.
AndroidManifesté um<memory-budget>declarado no manifesto do app,MemoryBudgetManageré um orçamento definido pelo app no momento da execução,MemoryLimiteré um limite de memória do sistema eam memory-limiteré uma substituição manual definida comadb. - Uso de pico: a maior cobrança desde que o memcg foi criado
(
memory.peak). Isso mostraN/Aem kernels que não são compatíveis. - Troca usada: memória compactada em ZRAM (
memory.swap.current). - Detalhamento de uso: os campos principais de
memory.stat, como memória anônima e com backup de arquivos. - Pressão e recuperação: os eventos de limitação contam quantas vezes o memcg
ultrapassou o limite de
memory.high. Refaults conta as páginas que foram trocadas ou removidas e depois foram necessárias novamente. Se esses números aumentarem enquanto você usa o app, o limite é mais restrito do que o conjunto de trabalho do app.
Com a flag -a ou -v, a seção também lista todos os contadores brutos de
memory.stat e memory.events.
Exercício prático: triagem rápida com meminfo
Este exercício mostra como diferentes tipos de alocações aparecem no meminfo.
Inicie o app:
adb shell am start -W -n com.android.memorylab/.MainActivityVerificar a memória de referência:
adb shell dumpsys meminfo -s com.android.memorylabA flag
-sfornece um resumo curto e conciso das categorias de memória.Allocate Java Memory: toque no botão Allocate Java Memory (10MB) três vezes no app.
Observe a mudança: execute
meminfo -snovamente. O valor do heap Java vai aumentar em aproximadamente 30.000 KB (30 MB).Alocar memória nativa: toque no botão Alocar memória nativa (1 GB não compactável) uma vez.
Observe a mudança: execute
meminfo -snovamente. O valor de Native Heap agora vai mostrar um aumento enorme (~1.000.000 KB).Comparar com o memcg (Android 18 e versões mais recentes): consulte a seção Memory Budget no final da saída de
meminfo -s. O Uso atual aumenta junto com o PSS TOTAL após cada alocação, porque as alocações Java e nativas são cobradas do memcg do app.
Visualização global
Para ver o uso da memória em todo o sistema, execute:
adb shell dumpsys meminfo
Isso fornece um resumo do uso de RAM, incluindo a RAM total, a RAM livre e uma lista de processos classificados por PSS.
Principais métricas do sistema
- ZRAM: troca compactada na RAM.
- DMA-BUF: buffers de memória compartilhada e gráficos. As versões modernas do Android usam
o DMA-BUF extensivamente para renderização de interface. Se o total de
DMA-BUFfor muito alto, investigue processos com muitos gráficos usando/sys/kernel/debug/dma_buf/bufinfo. - RAM perdida: memória que não é contabilizada pelo kernel ou por qualquer processo (por exemplo, regiões reservadas para hardware).
Estatísticas de memcg
Cada processo de app tem o próprio grupo de controle de memória (memcg), e o kernel mantém os totais de memória contabilizados nele (consulte memcg para saber como essa contabilização difere de RSS e PSS). A leitura desses arquivos é barata e não interfere no app. Portanto, é possível fazer uma amostragem deles a cada segundo enquanto você executa um fluxo do usuário.
Procure o caminho memcg de um processo na linha 0:: de /proc/<PID>/cgroup,
em vez de codificar o caminho. Transmita cada comando do dispositivo para adb shell como uma
string entre aspas simples para que o shell do dispositivo veja as aspas:
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"
Os arquivos principais são:
memory.current: memória cobrada do processo, em bytes. Esse é o número que os orçamentos de memória limitam.memory.swap.current: memória compactada em ZRAM, em bytes.memory.stat: um detalhamento da cobrança, em bytes para tamanhos.anoné memória anônima, como os heaps Java e nativo.fileé o cache de página com suporte de arquivo, como código e recursos.workingset_refault_anoneworkingset_refault_filecontam as páginas que foram trocadas ou removidas e que foram necessárias novamente. O aumento da contagem de refaults significa que o processo está com pouca memória.memory.events: contagens de eventos de limite de memória.highconta quantas vezes o processo foi limitado por ultrapassar o limite dememory.high.
O diretório pai do memcg do processo pertence ao app inteiro (o UID dele). Os arquivos dele incluem a memória de todos os processos em execução do app, além de páginas de arquivos em cache que sobraram de processos anteriores do app que foram encerrados e ainda não foram recuperados:
adb shell "cat $CG/../memory.current"
Um app pode consultar o próprio uso do cgroup em processo usando
MemoryBudgetManager. Se você receber um erro de permissão ao inspecionar
os arquivos cgroup de um app em adb shell, execute adb root em um build userdebug ou
em uma imagem de emulador sem o Google Play e tente novamente.
Para um tutorial guiado desses arquivos com o app de exemplo MemoryLab, consulte Exercício prático: como explorar o memcg.
showmap
Para uma análise muito mais detalhada dos mapeamentos de memória de um processo, use
showmap. Essa ferramenta lê de /proc/<pid>/smaps.
adb shell showmap <pid>
showmap lista todos os mapas de memória no processo. Os principais mapeamentos a serem procurados
incluem:
[anon:scudo:primary]ou[anon:libc_malloc]: o heap nativo. As versões modernas do Android usam o alocador Scudo.*.art: imagens de heap do Java (por exemplo, imagem de inicialização).[anon:dalvik-main space]: o heap principal do Java.*.so: bibliotecas compartilhadas.*.{oat,dex,odex,vdex}: código DEX compilado.
É possível classificar a saída para encontrar os maiores consumidores de memória particular:
# Sort by private clean + private dirty memory
adb shell showmap <pid> | awk '{print $6+$7, $0}' | sort -rn | head -n 10
Exemplo de resposta:
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]
...
Exercício prático: investigação com o showmap
Aprenda a identificar mapeamentos de memória específicos e diferenciar entre memória limpa e suja.
- Mmap Clean Memory: toque no botão Mmap Clean Anon Memory (10MB) no app MemoryLab uma vez.
Receber o ID de processo: a interface do app mostra o ID de processo. Também é possível encontrá-lo com este comando:
adb shell pidof com.android.memorylabInspecionar com showmap:
# Replace <PID> with the actual process ID from the previous step adb shell showmap <PID> | grep "memorylab-clean"Identifique o mapeamento: você verá um mapeamento anônimo rotulado como
[anon:memorylab-clean]com um tamanho virtual de exatamente 10.240 KB. O valor private dirty para esse mapa provavelmente é 0. Essa é uma memória anônima "limpa" porque usamosmmappara reservar o espaço virtual, mas ainda não gravamos nele. Portanto, nenhuma RAM física é realmente consumida.Comparação de heap nativo: compare isso com os mapeamentos de
[anon:scudo:primary], que teriam aparecido emshowmapse você tivesse executado após a etapa 5 do exercício dememinfo. Como essa alocação nativa de 1 GB grava em todas as páginas, elas são "sujas" e aparecem em grande quantidade na coluna private dirty.Dica profissional (objetos grandes): observe as alocações de heap Java do exercício
meminfo. Como são matrizes de bytes de 10 MB, elas provavelmente aparecem emshowmapcomo[anon:dalvik-free list large object space]. O Android Runtime (ART) coloca objetos maiores que 12 KB em um espaço de objeto grande (LOS, na sigla em inglês) separado para evitar a fragmentação no heap principal.
procstats
Enquanto o meminfo fornece um snapshot em um momento específico, o procstats fornece dados históricos
ao analisar periodicamente o estado e o uso de memória de todos os aplicativos
em execução. Essa é a principal ferramenta usada para monitorar a integridade da memória de longo prazo
e é o que impulsiona os relatórios de uso da memória nas Configurações do Android.
# View stats for the last 3 hours
adb shell dumpsys procstats --hours 3
Noções básicas sobre os estados
A saída de procstats pode ser muito grande. A parte mais importante são as
métricas entre parênteses ao lado de cada estado do processo:
Top: 100% (54MB-64MB-67MB / 49MB-58MB-61MB / 110MB-120MB-130MB over 4)
O formato é: (% de tempo no estado) (minPss-avgPss-maxPss / minUss-avgUss-maxUss / minRss-avgRss-maxRss em amostras)
- Pss (Proportional Set Size): a memória compartilhada é dividida proporcionalmente entre os processos.
- Uss (Unique Set Size): memória usada apenas por esse processo (equivalente a Private Clean + Private Dirty).
- Rss (Resident Set Size): memória física total usada, incluindo todas as páginas compartilhadas (tende a contar em excesso).
Por que isso é importante:
- App estável: os valores mínimo, médio e máximo estão próximos.
- Vazamento de memória: o máximo é significativamente maior que o mínimo, e a média aumenta ao longo do tempo conforme a contagem de amostras aumenta.
- Uso com picos: o máximo é muito maior que a média, indicando operações pesadas temporárias (como processamento de imagens) que podem causar OOMs se acontecerem no momento errado.
Estados do processo
O procstats rastreia apps em vários estados (geralmente abreviados):
- Persist: processos críticos do sistema que estão sempre em execução.
- Parte de cima: o app está em primeiro plano e visível para o usuário.
- ImpFg (primeiro plano importante): processos críticos em segundo plano (como IMEs) ou apps com um serviço em primeiro plano.
- ImpBg (segundo plano importante): processos em segundo plano que são considerados importantes para o sistema.
- Fgs / BFgs: serviços em primeiro plano ou serviços em primeiro plano vinculados.
- Service / ServRst: serviços em segundo plano ou serviços que estão sendo reiniciados.
- Em cache / LastAct: apps que estão em cache em segundo plano para reinicialização rápida. O sistema pode encerrar esses processos com segurança.
Estado da memória do dispositivo e estatísticas de tempo de execução
O procstats também monitora quanto tempo o dispositivo inteiro passou sob pressão de
memória. Procure a seção Estatísticas de tempo de execução:
Run time Stats:
SOff/Norm: +1h10m15s
SOn/Norm: +15m20s
Low: +2m10s
Crit: +45s
- SOff / SOn: tela desligada x tela ligada.
- Norm / Mod / Low / Crit: níveis de pressão de memória (Normal, Moderado, Baixo, Crítico).
Se um dispositivo passar muito tempo nos estados Low ou Crit, ele vai parecer lento, já que o kernel vai recuperar a memória de forma agressiva e o Low Memory Killer (LMK) vai começar a encerrar os processos em segundo plano. Em seguida, analise as estatísticas por pacote para ver quais apps têm alto uso da memória especificamente quando o dispositivo está nesses estados de estresse.
← Conceitos | ↑ Voltar | Java →