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 compartilhada com outros processos (como a imagem de inicialização do Zigoto). 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.
  • 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 criar o caminho por conta própria, porque o layout do cgroup pode ser diferente entre os dispositivos. Transmita cada comando do dispositivo para adb shell como uma única string entre aspas 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"

Se você receber um erro de permissão, execute adb root e tente novamente. Alguns dispositivos só permitem ler esses arquivos como raiz.

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 →