Liberar um objeto no código do app ou fechar um stream temporário nem sempre retorna a RAM física ao sistema operacional. Alocadores nativos personalizados, ambientes de execução com coleta de lixo e tabelas de hash de endereçamento aberto frequentemente retêm arenas de memória virtual que permanecem apoiadas por páginas físicas muito depois que os dados lógicos dentro delas desaparecem. Da mesma forma, ler ou mapear na memória grandes arquivos de recursos preenche o cache de páginas do kernel, que permanece carregado no grupo de controle de memória (memcg, na sigla em inglês) do app.
Nesta página, explicamos como usar as chamadas do sistema madvise e posix_fadvise para
retornar páginas anônimas e com suporte de arquivos não usadas ao SO, mantendo as reservas de endereços
virtuais e os descritores de arquivos intactos, e como verificar o impacto
no adb shell e no Perfetto.
Por que a desalocação lógica retém a RAM física
O kernel do Linux rastreia a memória física na granularidade das páginas de hardware,
que são de 4 KB ou 16 KB em dispositivos Android. Quando um
processo reserva uma região de espaço de endereço virtual com mmap, o kernel
cria uma área de memória virtual (VMA, na sigla em inglês) sem alocar RAM física. O kernel
aloca e cobra uma página física somente quando o processo toca essa página
pela primeira vez. Para memória anônima, uma primeira leitura mapeia uma página zero compartilhada, de modo que apenas uma gravação aloca RAM.
Depois que uma página é carregada na RAM, o kernel não sabe se os bytes dentro dela ainda representam objetos ativos no ambiente de execução do app. Dois padrões comuns mantêm as páginas físicas residentes depois que o app termina de usá-las:
- Arenas e pools anônimos: alocadores nativos personalizados (como alocadores de slab,
pool ou arena de frame), ambientes de execução gerenciados com espaços de heap
não móveis e tabelas de hash grandes reservam intervalos de endereços virtuais contíguos com
mmapusandoMAP_PRIVATE | MAP_ANONYMOUS. Quando o app redefine uma arena, libera um bloco de volta para um pool personalizado ou limpa uma tabela de hash zerando os metadados, as páginas subjacentes permanecem sujas. Sem uma dica explícita para o kernel, essas páginas continuam a contar para o tamanho do conjunto residente (RSS) e o tamanho do conjunto proporcional (PSS) anônimos do processo e para a cobrança do cgroupmemory.current. Sob pressão de memória, o kernel as compacta em ZRAM, onde elas ainda ocupam espaço de troca cobrado dememory.swap.current. - Cache de página com suporte a arquivos do streaming de recursos: quando o app lê grandes
pacotes de recursos, arquivos de nível, contêineres de mídia ou modelos de machine learning
usando
read,preadoummapcom suporte a arquivos, o kernel armazena em cache todas as páginas lidas no cache de página e cobra essas páginas do contadorfileno arquivomemory.statdo cgroup do processo. Se o app descompactar ou copiar esse recurso para um buffer de ambiente de execução anônimo ou fizer upload dele para a GPU, manter as páginas de arquivos brutos no cache de páginas vai duplicar o recurso na RAM.
Liberar páginas anônimas
A chamada do sistema madvise dá ao kernel conselhos sobre como o processo espera
usar um intervalo de endereços virtuais. Para mapeamentos anônimos particulares (MAP_PRIVATE |
MAP_ANONYMOUS), transmitir MADV_DONTNEED instrui o kernel a descartar imediatamente
as páginas físicas e todas as entradas de ZRAM trocadas que dão suporte ao intervalo
especificado alinhado à página.
Depois que madvise com MADV_DONTNEED retornar:
- O intervalo de endereços virtuais permanece reservado e válido no espaço de endereço do processo (
VmSizenão muda), então os ponteiros e os limites da arena permanecem intactos sem chamarmunmapoummap. - As páginas físicas são imediatamente desmapeadas e liberadas, reduzindo
RssAnon,Pss, o contadoranonemmemory.statememory.swap.currentpara todas as páginas que estavam na ZRAM. - Qualquer leitura ou gravação subsequente em um endereço nesse intervalo falha em uma nova página preenchida com zeros sob demanda.
| Sinalização de conselho | Mapeamento de destino | Comportamento do kernel no Android |
|---|---|---|
MADV_DONTNEED |
Particular anônimo (MAP_PRIVATE | MAP_ANONYMOUS) |
Descarta imediatamente páginas físicas e slots de troca de ZRAM. Os acessos
subsequentes falham em páginas preenchidas com zeros. Substituição determinística em
RssAnon e memory.current. |
MADV_DONTNEED |
Com suporte de arquivo (MAP_PRIVATE ou MAP_SHARED) |
Desmapeia páginas das tabelas de páginas do processo (RssFile
drops). As páginas particulares de cópia em gravação em um mapeamento MAP_PRIVATE
são descartadas, de modo que as modificações são perdidas e o próximo acesso lê novamente o
arquivo. As páginas MAP_SHARED sujas são gravadas de volta no armazenamento
mais tarde. As páginas limpas permanecem no cache até serem removidas. |
MADV_FREE |
Particular anônimo (MAP_PRIVATE | MAP_ANONYMOUS) |
Marca as páginas como reutilizáveis de forma lenta. O kernel retém as páginas em
RssAnon se não houver pressão de memória e as recupera apenas
quando a recuperação global ou memcg é executada. |
MADV_WILLNEED |
Anônimo com backup de arquivo ou trocado | Inicia a leitura antecipada assíncrona para mapeamentos com backup de arquivos ou lê páginas anônimas trocadas da ZRAM de volta para o cache de troca antes do tempo. |
Alinhar intervalos internos ao tamanho da página do ambiente de execução
O madvise opera em páginas inteiras. Ele requer um endereço inicial alinhado à página e
arredonda o comprimento até o próximo limite de página. Se você liberar um período dentro de uma
arena compartilhada em que os bytes vizinhos ainda contêm objetos ativos, arredondar o intervalo
para fora pode zerar as alocações ativas que compartilham as páginas de limite.
Sempre consulte o tamanho da página de execução com sysconf(_SC_PAGESIZE) para oferecer suporte a dispositivos de página de 4 KB e 16 KB e arredonde o endereço de início interno até o próximo limite de página e o endereço de término interno até o limite de página anterior. Para mais orientações sobre alocadores independentes de tamanho de página, consulte
Otimizar alocadores de memória personalizados.
#include <sys/mman.h>
#include <unistd.h>
#include <cstdint>
// Reclaims physical pages strictly inside [region_start, region_start + length)
// without disturbing live objects on partially occupied boundary pages.
void ReleaseUnusedArenaSpan(void* region_start, size_t length) {
const uintptr_t page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
const uintptr_t page_mask = ~(page_size - 1);
const uintptr_t raw_start = reinterpret_cast<uintptr_t>(region_start);
const uintptr_t raw_end = raw_start + length;
// Round start UP and end DOWN so only fully unused pages are discarded.
const uintptr_t aligned_start = (raw_start + page_size - 1) & page_mask;
const uintptr_t aligned_end = raw_end & page_mask;
if (aligned_end > aligned_start) {
madvise(reinterpret_cast<void*>(aligned_start),
aligned_end - aligned_start,
MADV_DONTNEED);
}
}
Ao criar mapeamentos anônimos personalizados com mmap, rotule a VMA com prctl
usando PR_SET_VMA_ANON_NAME para rastrear a residência dela por nome em
showmap e /proc/<pid>/smaps. dumpsys
meminfo conta as regiões anônimas com nomes personalizados em Unknown.
Alguns kernels mais antigos mantêm um ponteiro para o nome em vez de copiá-lo. Portanto, transmita uma
string que permaneça válida durante o ciclo de vida do mapeamento, como uma string
literal:
#include <sys/mman.h>
#include <sys/prctl.h>
#ifndef PR_SET_VMA
#define PR_SET_VMA 0x53564d41
#define PR_SET_VMA_ANON_NAME 0
#endif
void* ReserveNamedArena(size_t capacity_bytes, const char* arena_name) {
void* ptr = mmap(nullptr, capacity_bytes, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr != MAP_FAILED) {
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME,
reinterpret_cast<unsigned long>(ptr), capacity_bytes,
reinterpret_cast<unsigned long>(arena_name));
}
return ptr;
}
Remover páginas de cache de arquivos
Quando o app lê dados de um descritor de arquivo, o kernel armazena em cache as páginas do arquivo no cache de páginas para leituras futuras. Embora isso acelere os acessos repetidos a pequenos arquivos de configuração ou bibliotecas compartilhadas, ele funciona contra você ao transmitir grandes arquivos de recursos, pacotes de textura ou blocos de áudio e vídeo que são analisados uma vez e armazenados em formato decodificado em outro lugar.
A chamada do sistema posix_fadvise permite que o app informe ao kernel sobre o padrão de acesso esperado em um descritor do arquivo aberto:
POSIX_FADV_DONTNEED: instrui o kernel de que o intervalo de bytes especificado não será acessado em um futuro próximo. O kernel remove imediatamente as páginas em cache limpas e não mapeadas nesse intervalo alinhado à página do cache de páginas, reduzindo o contadorfileemmemory.state a cobrança dememory.currentdo seu cgroup.POSIX_FADV_SEQUENTIAL: dobra a janela de leitura antecipada do kernel para o descritor do arquivo, melhorando a capacidade de processamento ao transmitir um arquivo grande do início ao fim. Combine isso comPOSIX_FADV_DONTNEEDem blocos finais à medida que você os consome para que o consumo de cache da página permaneça limitado a uma pequena janela deslizante.POSIX_FADV_RANDOM: desativa a leitura antecipada especulativa do kernel no descritor do arquivo. Use isso ao realizar pesquisas esparsas e não contíguas por deslocamento de bytes em um arquivo grande ou arquivo de banco de dados para que cada leitura pequena não puxe páginas adjacentes não utilizadas para o cache de páginas. Para arquivos mapeados em memória, chamemadvisecomMADV_RANDOMno mapeamento.
O exemplo a seguir faz streaming de um grande arquivo de recursos em blocos, permitindo a leitura antecipada sequencial e a remoção de cada período alinhado à página processado do cache de página:
#include <fcntl.h>
#include <unistd.h>
#include <cstdint>
#include <vector>
void StreamAssetArchive(int fd, size_t total_size) {
const size_t page_size = static_cast<size_t>(sysconf(_SC_PAGESIZE));
constexpr size_t kChunkSize = 4 * 1024 * 1024; // 4 MB streaming window
std::vector<uint8_t> buffer(kChunkSize);
// Hint sequential access so the kernel prefetches upcoming chunks.
posix_fadvise(fd, 0, total_size, POSIX_FADV_SEQUENTIAL);
off_t offset = 0;
while (offset < static_cast<off_t>(total_size)) {
ssize_t bytes_read = pread(fd, buffer.data(), buffer.size(), offset);
if (bytes_read <= 0) break;
// Decode or upload buffer[0 .. bytes_read) into runtime structures here.
// Evict full pages that have been completely consumed.
const size_t aligned_len =
(static_cast<size_t>(bytes_read) / page_size) * page_size;
if (aligned_len > 0) {
posix_fadvise(fd, offset, static_cast<off_t>(aligned_len),
POSIX_FADV_DONTNEED);
}
offset += bytes_read;
}
}
Recuperar regiões mmap com suporte de arquivo
Se o app mapear na memória um arquivo de recurso grande com mmap em vez de ler
com pread, chamar posix_fadvise apenas com POSIX_FADV_DONTNEED não
vai remover as páginas que ainda estão mapeadas nas tabelas de páginas do processo. Para descartar um
período consumido de um mapeamento de arquivo somente leitura sem desmapear toda a VMA:
- Chame
madvise(addr, length, MADV_DONTNEED)no intervalo de endereços virtuais mapeados para desmapear essas páginas das tabelas de páginas do processo (RssFilecai). - Chame
posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED)no deslocamento de arquivo correspondente para que o kernel também remova as páginas limpas não mapeadas do cache de páginas (fileemmemory.statdrops).
Siga as diretrizes de conselhos para páginas
Siga estas práticas ao integrar madvise e posix_fadvise a
alocadores, carregadores de recursos e gerenciadores de ciclo de vida:
- Agrupar chamadas em regiões aproximadas: cada chamada
madviseusa o bloqueio do mapa de memória do processo (mmap_lock), percorre as tabelas de páginas e invalida as entradas correspondentes do buffer de consulta de tradução (TLB, na sigla em inglês) em outros núcleos de CPU (com instruções de manutenção de TLB de transmissão no Arm e interrupções entre processadores no x86). Não chamemadviseem todas as pequenas desalocações. Agrupe os intervalos livres e informe o kernel em blocos de 64 KB a 1 MB ou mais, ou limpe os intervalos livres durante os limites de frame e as transições em segundo plano (onStopeTRIM_MEMORY_UI_HIDDEN). - Recuperar capacidade vazia em tabelas de hash e pools: quando uma tabela de hash,
um pool de objetos ou um buffer de preparo grande é limpo após uma transição de cena ou
sincronização de dados, reduza a alocação de apoio ou emita
MADV_DONTNEEDna matriz de apoio limpa. Manter uma tabela de endereçamento aberto de 32 MB alocada com marcadores de exclusão mantém todos os 32 MB de páginas físicas cobrados do seu processo. - Limpar dados de arquivos sujos antes de removê-los:
o
POSIX_FADV_DONTNEEDsó remove páginas limpas do cache de páginas. Se o app gravar um arquivo temporário ou de cache grande que não será lido imediatamente, chamefdatasyncantes deposix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)para que as páginas sujas sejam gravadas no armazenamento e, em seguida, descartadas da RAM. - Não remova códigos ou recursos compartilhados ativos: evite chamar
POSIX_FADV_DONTNEEDem DEX, ODEX, VDEX, bibliotecas compartilhadas (.so) ou recursos de interface que são acessados repetidamente. A remoção de páginas de código ou recursos ativos força falhas de armazenamento síncronas que travam as linhas de execução e aumentam a pressão de E/S, conforme relatado pelas informações de travamento por pressão (PSI, na sigla em inglês).
Verificar a recuperação de páginas no shell
Verifique se madvise e posix_fadvise recuperam a RAM física usando showmap
e os arquivos de contabilização do cgroup v2 do app.
Inspecionar o tamanho virtual em comparação com o físico com o showmap
Quando você nomeia uma arena anônima com PR_SET_VMA_ANON_NAME, o showmap mostra
a reserva virtual (virtual size) e as páginas físicas residentes
(RSS e PSS). O exemplo de saída abaixo vem do app de exemplo
MemoryLab no AOSP, que nomeia a arena como memorylab-arena. Substitua com.example.app
e memorylab-arena pelo nome do pacote e da arena. Antes de chamar
madvise com MADV_DONTNEED em uma arena de 64 MB tocada, execute:
adb shell "showmap \$(pidof com.example.app) | grep -E '^ +virtual|^ +size|memorylab-arena' | head -3"
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
65536 65536 65536 0 0 0 65536 0 0 0 0 0 0 0 0 1 [anon:memorylab-arena]
Depois de chamar madvise(arena_ptr, 64 * 1024 * 1024, MADV_DONTNEED), execute
showmap novamente:
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
65536 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 [anon:memorylab-arena]
O virtual size permanece em 65536 KB (64 MB), confirmando que o
intervalo de endereços virtuais ainda está reservado, enquanto RSS, PSS e private dirty
caem para 0 KB.
Inspecionar memory.current e memory.stat do cgroup
Para verificar a recuperação de páginas anônimas e a remoção do cache de páginas não mapeadas
(que não aparecem nas tabelas de páginas do processo se lidas com pread), inspecione o
diretório cgroup v2 do processo. No Android, cada processo de app tem o próprio cgroup
(como /sys/fs/cgroup/apps/uid_<UID>/pid_<PID>/, dependendo do dispositivo).
O comando a seguir encontra o diretório na linha 0:: de
/proc/<PID>/cgroup e imprime os contadores:
adb shell '
PID=$(pidof com.example.app)
CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
echo "memory.current: $(cat $CG/memory.current)"
echo "memory.swap.current: $(cat $CG/memory.swap.current)"
grep -E "^(anon|file) " $CG/memory.stat
'
Antes de liberar uma arena anônima de 64 MB e remover um arquivo de recurso mapeado em memória de 128 MB do cache de página:
memory.current: 242962432
memory.swap.current: 16633856
anon 100671488
file 140009472
Imediatamente após chamar madvise com MADV_DONTNEED na arena e no
mapeamento de arquivos de recursos e, em seguida, posix_fadvise com POSIX_FADV_DONTNEED no
descritor do arquivo de recursos:
memory.current: 41766912
memory.swap.current: 16502784
anon 33722368
file 5914624
anon cai em 64 MB e file cai em 128 MB, reduzindo
memory.current em cerca de 192 MB sem fechar o descritor do arquivo ou
remover o mapeamento do espaço de endereço virtual da arena. O memory.swap.current quase não
muda, porque quase nenhuma das páginas da arena foi trocada para a ZRAM. Os
valores dependem do dispositivo e de quanto do arquivo o kernel já
removeu por conta própria. Para saber mais sobre como o kernel cobra e recupera
memória por cgroup, consulte Recuperação de memória: remoção e troca.
Analisar a recuperação de páginas no Perfetto
O Perfetto permite correlacionar chamadas madvise e posix_fadvise com contadores de tabela de páginas e cache de páginas do kernel em uma linha do tempo unificada.
Gravar um rastro
Capture um rastro com linux.process_stats para contadores de processos periódicos e
linux.ftrace para eventos de RSS e cache de página em tempo real e, em seguida, extraia-o para
seu computador:
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/page_advice.perfetto-trace <<EOF
buffers: {
size_kb: 65536
fill_policy: RING_BUFFER
}
data_sources: {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
proc_stats_poll_ms: 100
}
}
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "kmem/rss_stat"
ftrace_events: "filemap/mm_filemap_add_to_page_cache"
ftrace_events: "filemap/mm_filemap_delete_from_page_cache"
}
}
}
duration_ms: 20000
EOF
adb pull /data/misc/perfetto-traces/page_advice.perfetto-trace
Inspecionar faixas na interface do Perfetto
Abra o rastro na interface do Perfetto, expanda o grupo de processos do app e fixe as seguintes faixas de contador para que elas fiquem na parte de cima da linha do tempo:
mem.virt: permanece estável em todas as chamadas deMADV_DONTNEED, confirmando que a reserva de endereço virtual (mmap) não mudou.mem.rss.anon: diminui drasticamente no carimbo de data/hora exato em que o app chamamadvisecomMADV_DONTNEEDem arenas anônimas sujas ou regiões de heap.mem.rss.file: diminui quando o app chamamadvisecomMADV_DONTNEEDem uma regiãommapcom suporte de arquivo, acompanhada por uma explosão de eventos ftracemm_filemap_delete_from_page_cachequandoposix_fadvisecomPOSIX_FADV_DONTNEEDremove as páginas limpas do cache de páginas.mem.swap: cai quando oMADV_DONTNEEDdescarta páginas anônimas que foram compactadas anteriormente na ZRAM.
A Figura 2 mostra essas faixas para o app de exemplo MemoryLab. Para conferir a mesma visualização, abra o exemplo de rastro na interface do Perfetto.
Consultar o pico e a memória final com o PerfettoSQL
Execute a consulta PerfettoSQL abaixo na interface do Perfetto (guia Query (SQL)) para
comparar os valores de pico e finais de mem.virt, mem.rss.anon,
mem.rss.file e mem.swap para o processo de destino:
WITH counters AS (
SELECT t.name AS counter_name, c.ts, c.value
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name = 'com.example.app'
AND t.name IN ('mem.virt', 'mem.rss.anon', 'mem.rss.file', 'mem.swap')
)
SELECT
counter_name,
CAST(MAX(value) / 1048576 AS INT) AS peak_mb,
CAST((
SELECT value FROM counters AS last
WHERE last.counter_name = counters.counter_name
ORDER BY ts DESC LIMIT 1
) / 1048576 AS INT) AS final_mb
FROM counters
GROUP BY counter_name
ORDER BY counter_name;
Exemplo de saída para o trace na Figura 2:
counter_name peak_mb final_mb
mem.rss.anon 111 48
mem.rss.file 212 84
mem.swap 36 36
mem.virt 13952 13952
mem.rss.anon cai em 63 MB e mem.rss.file cai em 128 MB em relação aos
picos, enquanto mem.virt termina no pico porque as reservas
virtuais não mudam.
Resolver páginas removidas para arquivos
Os eventos mm_filemap_add_to_page_cache e mm_filemap_delete_from_page_cache
identificam arquivos pelo número do inode (i_ino), e não pelo caminho. Para confirmar
que as páginas removidas pertencem ao arquivo que você recomendou, conte as remoções por
inode:
SELECT
EXTRACT_ARG(arg_set_id, 'i_ino') AS inode,
COUNT(*) AS evicted_pages
FROM ftrace_event
WHERE name = 'mm_filemap_delete_from_page_cache'
GROUP BY inode
ORDER BY evicted_pages DESC
LIMIT 5;
Exemplo de saída para o trace na Figura 2:
inode evicted_pages
9761 35781
76792192 767
126 668
0 612
11587712 514
Enquanto o dispositivo que gravou o trace ainda estiver conectado, resolva cada inode para um caminho. Os números de inode são exclusivos apenas em um sistema de arquivos. Portanto, limite a pesquisa ao sistema de arquivos que contém os dados do app:
INODE=<inode from the query>
adb shell "find /data -xdev -inum $INODE 2>/dev/null"
Exemplo de saída para o inode 9761:
/data/user/0/com.android.memorylab/files/asset_pack.bin
/data/data/com.android.memorylab/files/asset_pack.bin
Os dois caminhos se referem ao mesmo arquivo, porque /data/user/0 e /data/data são
dois nomes para o mesmo diretório de dados do app. A contagem de remoção do arquivo de recurso (35.781 páginas) é maior que o próprio arquivo (32.768 páginas de 4 KB), porque o kswapd já havia removido parte do arquivo sob pressão de memória antes que o app o lesse novamente e chamasse posix_fadvise.
Para um script que resolve muitos inodes de uma só vez, consulte Como resolver falhas em inodes e, em seguida, em arquivos.
← Interação entre kswapd e lmkd | ↑ Para cima | Processo em segundo plano enxuto →