Para entender o uso da memória no Android, é preciso analisar o sistema de várias perspectivas diferentes, desde objetos Java de alto nível até páginas de kernel de baixo nível.
A queda de performance
O uso da memória em si não é algo bom ou ruim. O que importa é para que você está usando a memória. No entanto, ao se aproximar do limite de memória disponível em um dispositivo, você acaba "caindo em um abismo de desempenho".

Quando você está longe do limite, adicionar uma pequena quantidade de uso da memória (por exemplo, 50 MB) pode não ter um impacto perceptível no desempenho. No entanto, um limite é atingido quando a RAM disponível é esgotada. Nesse ponto, o sistema operacional precisa começar a paginar a memória e encerrar processos para liberar espaço. Além desse limite, até mesmo um pequeno aumento no uso da memória pode levar a uma "sobrecarga" significativa, em que o dispositivo deixa de responder ou parece reiniciar porque processos críticos são encerrados.
A pilha do Android
Cada camada da pilha do Android tem uma visualização exclusiva da memória:

- Aplicativos (Java/Kotlin): os desenvolvedores veem principalmente objetos Java alocados no heap Java.
- Android Runtime (ART): o ART gerencia o heap Java/Kotlin usando páginas de memória virtual do kernel. Anteriormente conhecido como Dalvik (os termos às vezes são usados de forma intercambiável).
- Kernel do Linux: o kernel vê a memória em termos de páginas físicas e virtuais. Tradicionalmente, elas têm 4 KB, mas o Android também oferece suporte a tamanhos de página de 16 KB. O kernel não sabe nada sobre "objetos Java".
Se você quiser otimizar a memória, faça isso na sua própria camada (por exemplo, carregando menos bitmaps) ou entenda as camadas abaixo para ver como as alocações de alto nível se traduzem no uso da página física.
Tipos de memória: anônima x com backup em arquivo
Antes de entender como o SO recupera memória, é preciso entender as duas categorias fundamentais de páginas de memória no Linux:
- Memória anônima (
anon): memória que não tem um arquivo de backup no armazenamento. Isso inclui a alocação de memória pormallocem C/C++ (como o alocador Scudo) e a alocação de memória para objetos Java no heap Java/Kotlin. Como essa memória não tem um arquivo de origem para retornar, o SO precisa mantê-la na RAM ou compactá-la e trocá-la por ZRAM quando estiver sob pressão. - Memória com suporte de arquivo (
file): memória mapeada diretamente de um arquivo no armazenamento. Isso inclui código executável (arquivos DEX, bibliotecas nativas.so) e fontes.
Arquivos mapeados de memória (mmap)
O Android usa mmap para mapear a memória anônima e com suporte de arquivos no
espaço de endereço de um processo.
Ao lidar com memória de arquivos, as páginas são classificadas em:
- Memória limpa: páginas mapeadas de um arquivo que não foram modificadas. Se o sistema precisar de mais memória, o kernel poderá simplesmente descartar essas páginas, já que elas podem ser recarregadas diretamente do arquivo no armazenamento mais tarde.
- Memória suja: páginas que foram modificadas pelo processo. Elas não podem ser descartadas, se comportam como memória anônima e precisam ser mantidas na RAM ou movidas para a ZRAM.
O Android prefere formatos de arquivo (como DEX) que sejam compatíveis com o mapeamento de memória, permitindo que o sistema recupere facilmente a memória limpa sob pressão. Como a memória anônima e suja não pode ser simplesmente descartada, uma alta quantidade de memória particular suja/anônima é a principal causa de thrashing do sistema e de intervenções do Low Memory Killer (LMK). Se o aplicativo usa muita memória particular suja, você está contribuindo diretamente para o limite de desempenho.
O modelo de processo Zygote
O Android minimiza o custo de inicialização de novos aplicativos usando um processo chamado Zygote.
- O Zygote é iniciado na inicialização e pré-carrega classes e recursos comuns do framework na memória. Do ponto de vista do kernel, isso se torna uma memória suja anônima, mas é exclusiva do processo do Zygote.
- Quando um novo aplicativo é iniciado, o sistema bifurca o processo Zygote.
- O novo processo filho herda a memória do Zygote usando um mapeamento compartilhado com semântica de cópia em gravação (COW, na sigla em inglês).

Enquanto o processo filho apenas ler a memória herdada do Zygote, as páginas de memória física permanecerão compartilhadas entre todos os processos. Quando um processo filho modifica uma página compartilhada, o kernel cria de forma transparente uma cópia privada dessa página para o processo. Esse modelo permite que muitos processos compartilhem uma grande parte da memória, especialmente o código e os recursos do framework, reduzindo significativamente o consumo geral de memória do sistema.
RSS, PSS, USS e memcg
Como a memória é muito compartilhada entre processos, principalmente pelo modelo Zygote, há várias maneiras de contabilizar o uso da memória de um processo. RSS, PSS e USS contam as páginas que são mapeadas no espaço de endereço de um processo:

- RSS (Resident Set Size): o número total de páginas que o processo tem na RAM. Isso superestima o uso porque conta as páginas compartilhadas várias vezes (uma para cada processo que as compartilha).
- PSS (Proportional Set Size): a quantidade total de memória exclusiva do processo, além da parcela proporcional de memória compartilhada. Se uma página for compartilhada por cinco processos, cada um deles será cobrado por 1/5 dessa página.
- USS (Unique Set Size): a quantidade de memória exclusiva do processo. Essa é a memória que seria retornada ao sistema se o processo fosse encerrado.
memcg: a própria contabilização do kernel
O kernel também mantém o próprio total de memória que cada processo
usa, esteja ela mapeada ou não. No Android, cada processo de app
pertence ao próprio grupo de controle de memória (memcg), aninhado em
um memcg para todo o app (consulte A hierarquia de memcg do Android).
Sempre que uma página de memória é alocada, o kernel a cobra do memcg do
processo que a alocou. A cobrança total está no arquivo memory.current
do memcg.
A contabilização do memcg é diferente do RSS, PSS e USS de algumas maneiras importantes:
- Cada página é cobrada exatamente uma vez, para o memcg do processo que a trouxe para a memória pela primeira vez. A cobrança permanece lá mesmo que outros processos mapeiem a mesma página posteriormente. As páginas que o Zigoto pré-carrega são cobradas dele, não do seu app. Quando o app grava em uma dessas páginas, a cópia particular é cobrada do app.
- Inclui memória que as métricas de tabela de páginas não conseguem ver.
memory.currentconta as páginas de arquivo que o app lê sem mapeá-las (por exemplo, páginas de banco de dados lidas comreadoupread). - Ele rastreia a troca separadamente. A memória compactada na ZRAM sai
de
memory.currente entra emmemory.swap.current, para que você possa ver as duas sem precisar adivinhar. - É barato para ler. Os contadores estão sempre atualizados, e a leitura deles não bloqueia nem verifica o espaço de endereço do app.
- É o que o sistema limita. Os limites e as cotas de memória que o Android aplica aos apps Android atuam na cobrança do memcg.
A memória gráfica, como os buffers por trás dos bitmaps de hardware, geralmente não é cobrada do memcg do app.
Quais métricas usar e quando?
A escolha da métrica de memória certa depende do que você está tentando medir e do ambiente em que está fazendo a medição. Em dispositivos com o Android 17 QPR2 (nível 37.2 da API) e versões mais recentes, comece com o memcg ao rastrear seu próprio app.
| Métrica | Ideal para | Força da chave | Principal desvantagem |
|---|---|---|---|
| memcg | Acompanhar um app individual ao longo do tempo, comparando com os orçamentos | Conta cada página uma vez, inclui o cache de páginas não mapeadas e rastreia a troca separadamente. Leitura barata. | Não disponível em todos os dispositivos anteriores ao Android 17 QPR2. As páginas compartilhadas são cobradas do processo que as acessou primeiro. |
| PSS | Snapshots de memória de todo o sistema | Proporciona a memória compartilhada com precisão. A soma do PSS é igual à memória total usada. | Flutua com base em outros processos em execução. Não é bom para comparar um app específico ao longo do tempo. |
| RSS | Rastrear um app individual ao longo do tempo, telemetria de campo | Estável, independente de outros processos. Rápido e barato de coletar. | Superestima o uso total porque conta a memória compartilhada várias vezes. |
| USS | Avaliar o impacto de encerrar um processo | Mostra exatamente quanta memória seria liberada se o app fosse encerrado. | Ignora totalmente a memória compartilhada. |
memcg (cobrança do grupo de controle de memória)
- Ideal para: monitorar o uso da memória de um app individual ao longo do tempo, em jornadas ideais do usuário (CUJs, na sigla em inglês) ou entre versões do app, em dispositivos com o Android 17 QPR2 ou mais recente. Essa também é a métrica a ser observada ao calibrar orçamentos de memória, porque é o número que os orçamentos limitam.
- Motivo: assim como o RSS, a carga do memcg não muda quando outros apps começam ou param de compartilhar páginas com o seu. Ao contrário do RSS, ele nunca conta uma página duas vezes, então você pode somar a carga de todos os processos em execução do seu app. O memcg de todo o app pode ser maior que essa soma, porque ele mantém as páginas de arquivos em cache dos processos encerrados até que o kernel as recupere. Ele também conta a memória que o RSS, o PSS e o USS não consideram, como o cache de arquivos não mapeados. A leitura é barata, porque o kernel atualiza os contadores à medida que carrega as páginas.
- Como ler: leia
memory.current,memory.swap.currentememory.statno diretório memcg de um processo (consulte estatísticas de memcg). No Android 18 e versões mais recentes, odumpsys meminfotambém as mostra. No app, useMemoryBudgetManager. - Práticas recomendadas de medição de campo: pelos mesmos motivos explicados em
RSS, rastreie a memória anônima + espaço de troca: o campo
anondememory.statmaismemory.swap.current. O campofiledememory.statinclui páginas de arquivos que o app leu uma vez e que o kernel ainda não recuperou. Portanto,memory.currentpode permanecer alto por um tempo depois que o app parar de usar essa memória. - Quando ter cuidado: como as páginas são cobradas do processo que
as acessou primeiro, seu app é cobrado por uma página de arquivo compartilhado (como uma
fonte ou uma biblioteca compartilhada) que ele lê antes de qualquer outro processo.
Por outro lado, outro processo pode ser cobrado por páginas que seu app também
usa. Espere pequenas diferenças entre dispositivos e execuções para memória com backup de arquivos
e use
anone swap ao comparar.
PSS (tamanho do conjunto proporcional)
- Ideal para: fazer um snapshot completo da memória do sistema de um determinado dispositivo em um determinado momento.
- Motivo: o PSS tem a propriedade matemática útil de que a soma do PSS para todos os processos em execução é igual à memória total usada pelos processos no sistema. Ele distribui perfeitamente a memória compartilhada sem contagem duplicada.
- Quando evitar: não use o PSS para comparar um processo específico em dois snapshots diferentes, em momentos diferentes ou em dispositivos diferentes. Como o PSS depende de quantos outros processos estão compartilhando páginas com o seu, o PSS do app pode variar, mesmo que o comportamento real do app permaneça completamente inalterado.
RSS (Resident Set Size)
- Ideal para: acompanhar o uso da memória de um app ao longo do tempo, em diferentes jornadas ideis do usuário (CUJs) ou comparar versões do app. Também é a escolha mais prática para telemetria de campo.
- Motivo: o RSS é mais estável que o PSS. Informa sobre a memória mapeada no seu processo, independentemente do que outros apps estejam fazendo. Além disso, o RSS é barato de coletar. A medição de PSS ou USS requer a verificação de estruturas complexas de gerenciamento de memória do kernel e a obtenção de bloqueios, o que pode causar problemas de desempenho do sistema (como instabilidade na interface) se coletados com muita frequência.
- Práticas recomendadas de medição de campo: ao coletar dados de dispositivos em
campo, geralmente é melhor medir o RSS anônimo + troca (ZRAM).
- Por que apenas anônimo? As páginas com backup de arquivos (como código, recursos de apps, fontes ou outros arquivos mapeados na memória) podem ser removidas pelo kernel a qualquer momento devido à pressão do cache de páginas em todo o sistema. Incluir páginas de arquivos em métricas para rastrear um app específico introduz ruído das decisões de gerenciamento de memória do SO, que podem ser impulsionadas pela pressão de memória de outros apps ou do sistema. Por outro lado, a memória anônima (como os heaps Java e nativo) é controlada diretamente pelo app.
- Por que adicionar swap? Sob pressão de memória, o SO compacta páginas anônimas e as move para a ZRAM (espaço de troca). Se você medir apenas a memória anônima residente, suas métricas poderão mostrar falsamente uma "diminuição" no uso da memória simplesmente porque o sistema estava sob pressão e trocou suas páginas. Adicionar a memória trocada garante que você contabilize toda a memória anônima que o app alocou, esteja ela atualmente na RAM ou na ZRAM.
- Prefira o memcg sempre que possível. Em dispositivos com o Android 17 QPR2 e
versões mais recentes,
anonemmemory.statmaismemory.swap.currentoferece uma medição semelhante que deixa de fora as páginas anônimas compartilhadas com o Zygote e é mais barata de coletar (consulte memcg).
USS (tamanho do conjunto exclusivo)
- Ideal para: determinar o impacto imediato no sistema ao encerrar um processo.
- Motivo: o USS representa com precisão a quantidade exata de memória que seria
imediatamente recuperada e retornada ao sistema se o processo fosse
encerrado (por exemplo, pelo daemon Low Memory Killer,
lmkd). - Como usar: essa métrica é muito útil ao avaliar processos persistentes ou serviços em segundo plano. Se você estiver avaliando o custo de um processo em segundo plano para todo o sistema, o USS informa exatamente quanta memória o sistema está sacrificando exclusivamente para manter esse processo específico ativo.