Ferramentas de avaliação rápida

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 os Native Heap e Dalvik Heap foram 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 SwapPss para 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 Activities e ViewRootImpl. Se você sair do app e forçar uma GC, a contagem de Activities vai 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 ou none, 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 e am memory-limiter é uma substituição manual definida com adb.
  • Uso de pico: a maior cobrança desde que o memcg foi criado (memory.peak). Isso mostra N/A em 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.

  1. Inicie o app:

    adb shell am start -W -n com.android.memorylab/.MainActivity
    
  2. Verificar a memória de referência:

    adb shell dumpsys meminfo -s com.android.memorylab
    

    A flag -s fornece um resumo curto e conciso das categorias de memória.

  3. Allocate Java Memory: toque no botão Allocate Java Memory (10MB) três vezes no app.

  4. Observe a mudança: execute meminfo -s novamente. O valor do heap Java vai aumentar em aproximadamente 30.000 KB (30 MB).

  5. Alocar memória nativa: toque no botão Alocar memória nativa (1 GB não compactável) uma vez.

  6. Observe a mudança: execute meminfo -s novamente. O valor de Native Heap agora vai mostrar um aumento enorme (~1.000.000 KB).

  7. 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-BUF for 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_anon e workingset_refault_file contam 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. high conta quantas vezes o processo foi limitado por ultrapassar o limite de memory.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.

  1. Mmap Clean Memory: toque no botão Mmap Clean Anon Memory (10MB) no app MemoryLab uma vez.
  2. 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.memorylab
    
  3. Inspecionar com showmap:

    # Replace <PID> with the actual process ID from the previous step
    adb shell showmap <PID> | grep "memorylab-clean"
    
  4. 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 usamos mmap para reservar o espaço virtual, mas ainda não gravamos nele. Portanto, nenhuma RAM física é realmente consumida.

  5. Comparação de heap nativo: compare isso com os mapeamentos de [anon:scudo:primary], que teriam aparecido em showmap se você tivesse executado após a etapa 5 do exercício de meminfo. 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.

  6. 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 em showmap como [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 →