O código que você escreve é uma forma de uso da memória. Cada classe, método e constante de string no seu aplicativo precisa ser carregada na RAM quando é executada. Quanto maior for a base de código do aplicativo, mais memória ele vai consumir apenas para existir.
Memória com suporte de arquivo e paginação por demanda
O Android carrega código executável do seu .apk (como arquivos .oat ou .so)
usando mmap. Isso significa que o código é baseado em arquivo.
O Android usa paginação por demanda. Quando o app é iniciado, o kernel não carrega todo o APK na RAM imediatamente. Em vez disso, ele mapeia apenas o arquivo no espaço de endereço virtual do processo. À medida que o app é executado e a CPU salta para uma nova função, ela aciona uma "falha de página". O kernel pausa a thread, lê essa página específica de 4 KB de código do armazenamento para a RAM física e retoma a execução.

Isso significa que o código que você empacota, mas nunca executa, não usa memória física para as próprias páginas de código. No entanto, as bibliotecas não usadas ainda aumentam o tamanho geral do APK e podem aumentar significativamente a memória usada pelos metadados internos do sistema (como índices DEX e descritores de classe), que precisam ser lidos para saber que o código existe. Além disso, muitas bibliotecas contêm inicializadores estáticos ou são afetadas por frameworks de injeção de dependência durante a inicialização do app, fazendo com que sejam paginadas na RAM de qualquer maneira.
Despejo e lentidão de páginas
Como a memória com suporte de arquivo sempre pode ser lida novamente do armazenamento, o kernel considera essas páginas "limpas". Quando o sistema sofre pressão de memória, o kernel remove (descarta) essas páginas de código limpo da RAM para abrir espaço para outras coisas.
Se o app precisar executar esse código novamente, a CPU vai falhar, e o kernel precisará ler a página novamente do armazenamento. Quanto mais código o app tiver, mais vulnerável ele fica à remoção do código. Quando um usuário volta para seu app inchado depois de usar outros apps, ele vai sentir instabilidade aleatória e lentidão porque a CPU fica constantemente parada esperando que o código seja paginado de volta do armazenamento.
O custo de uma falha de página:embora varie muito de acordo com a velocidade de armazenamento do dispositivo (UFS x eMMC) e o estado do kernel, uma falha de página grave (leitura de 4 KB do armazenamento) pode custar de 0,5 ms a 5 ms. Se o caminho de inicialização passar por 500 páginas diferentes de código não otimizado, você poderá facilmente introduzir vários centésimos de milissegundos de latência de E/S pura no tempo de inicialização do app.
Como analisar o tamanho do código com o Compiler Explorer
Para entender como seu código Java ou Kotlin é convertido em código de máquina nativo (e, portanto, bytes de memória), use o Compiler Explorer.
O suporte ao Android é integrado diretamente ao Godbolt. Ele permite ver como diferentes partes da cadeia de ferramentas do Android (D8, R8 e dex2oat) transformam seu código-fonte.
Como usar o Compiler Explorer com o Android
- Acesse godbolt.org.
- Selecione Android Java ou Android Kotlin no menu suspenso de linguagem (canto superior esquerdo).
- No menu suspenso do compilador (canto superior direito do painel de código), você pode escolher
entre diferentes ferramentas:
d8: mostra o bytecode Dalvik (.dex). Essa é a representação mais próxima do seu código original e é mais fácil de ler.r8: mostra como o otimizador R8 reduz e otimiza seu bytecode.dex2oat: mostra o código de máquina ARM64 final que é executado no dispositivo. É aqui que você pode ver o impacto real na memória (4 bytes por instrução). Odex2oatpode segmentar diferentes ISAs, mas o ARM64 é o mais comum para smartphones.
- Destaque de origem<>saída: ao passar o cursor sobre uma linha de código, o bytecode ou as instruções de código de máquina correspondentes serão destacados, facilitando o rastreamento do impacto de instruções específicas.
- Pipeline de otimização: na visualização de desmontagem, clique em Adicionar novo... -> Pipeline de ativação. Isso permite que você veja as etapas internas realizadas pelo compilador. Você pode inspecionar como a Representação interna (IR) é transformada em cada estágio (por exemplo, entre as etapas "Inliner (antes)" e "Inliner (depois)") antes de ser reduzida ao código de máquina ARM64 final.

Por que isso é importante para a memória
Cada instrução que você vê na saída dex2oat destinada à ISA ARM64 ocupa 4 bytes no arquivo executável do app (.odex ou .oat).
Tente inserir um código que use recursos de linguagem diferentes e estude a saída do compilador:
- Acesso a matrizes x iteradores de lista:
- Um loop de array simples em
int[]pode ser compilado em cerca de 10 instruções (~40 bytes). - Um loop foreach em um
Listusa implicitamente umIterator. Isso pode resultar em 30 a 40 instruções (~160 bytes) devido às chamadas de método extras (hasNext(),next()) e à alocação do objeto iterador em si. - Otimização do R8: nas condições certas (por exemplo, quando o
Listé comprovadamente umArrayList), o otimizador do R8 pode transformar um loop foreach de volta em um loop indexado simples, eliminando a sobrecarga do iterador e reduzindo o tamanho do código e a rotatividade de memória de tempo de execução.
- Um loop de array simples em
- Chamadas de método virtual: envolvem carregar a classe do objeto, encontrar o método no
vtablee, em seguida, ramificar. Isso geralmente leva de 4 a 5 instruções (~20 bytes). - Chamadas diretas/estáticas: geralmente se traduzem em uma única instrução
bl(ramificação com link) (4 bytes). - Lambdas do Kotlin: podem gerar classes anônimas inteiras e outros métodos de ponte, adicionando centenas de bytes de sobrecarga de código e metadados para um bloco funcional simples.
Ao usar o Compiler Explorer, você pode ver como recursos sofisticados da linguagem (como lambdas do Kotlin, APIs de fluxo ou uso intenso de genéricos) afetam o tamanho final compilado do aplicativo e como otimizadores como o R8 podem neutralizar o custo das abstrações de linguagem em alguns casos. Essa ferramenta pode ajudar você a fazer concessões informadas ao projetar e implementar um app.
De modo geral, mais complexidade no código do app leva a um uso maior da memória. Por outro lado, um código mais simples ou simplificado pelo R8 resulta em uma representação menor como instruções de CPU e bytes no armazenamento e na RAM.
Medir o impacto do código com meminfo e showmap
Você pode usar as ferramentas padrão de memória do Android para ver quanta memória o código do seu app está consumindo.
dumpsys meminfo
Ao executar adb shell dumpsys meminfo <package>, a categoria Código na seção Resumo do app fornece uma visão geral da memória relacionada ao código:
App Summary
Pss(KB)
------
Java Heap: 3244
Native Heap: 5412
Code: 24512 # <--- Sum of .so, .dex, .oat, .art, etc.
showmap
Para uma visualização mais granular, use showmap. Ele revela regiões de arquivos específicos sendo mapeados para a memória.
adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"
Você vai encontrar entradas para o código compilado do aplicativo:
size RSS PSS clean dirty clean dirty swap swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
12288 8192 8192 8192 0 0 0 0 0 /data/app/.../base.odex
Código morto e R8
Como cada método executado ocupa memória, ter um app "inchado" com inicializações desnecessárias ou bibliotecas não utilizadas pode afetar muito o desempenho de inicialização e o uso da memória de referência.
Por isso, ferramentas como o R8 (ProGuard) são essenciais. O R8 analisa o bytecode do aplicativo e remove todas as classes ou métodos que nunca são chamados ("remoção de código morto").
Exercício prático: o custo do bloat
Para demonstrar o impacto do tamanho do código, considere um experimento que compara dois builds de um aplicativo com 300 classes geradas (cada uma com 500 métodos):
- CodeBloat (não otimizado): o build padrão não otimizado que contém todas as classes geradas e strings exclusivas.
- CodeBloatOptimized: o mesmo código-fonte, mas compilado com a redução do R8 ativada.
1. Compilação antecipada (AOT)
Para maximizar o impacto da memória com suporte de arquivo, vamos usar a ferramenta cmd package compile
para compilar os apps antecipadamente (AOT) em arquivos .oat.
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized
Este é um exemplo sintético. Normalmente, os apps usam o modo de compilação
speed-profile (confira mais abaixo).
2. Lançar e comparar
Para ver uma inicialização realmente fria, em que o sistema precisa ler o código do armazenamento, vamos descartar o cache de página do kernel antes de iniciar cada app. Isso exige acesso root.
Inicie o app não otimizado:
adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat
Agora faça o mesmo para o app otimizado:
adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
Os resultados
Se você observar a linha Code na seção App Summary, vai notar uma diferença enorme:
- Não otimizado
Code: ~30.000 KB (30 MB) - Otimizado
Code: ~2.000 KB (2 MB)
Como o R8 determinou que os 500 métodos dentro dessas classes nunca faziam nada útil (o método doSomething() só chama method0(), e os resultados são ignorados), ele removeu quase todo o código gerado artificialmente do APK final.
3. Conferir o impacto no Perfetto
O impacto do aumento do código fica claramente visível durante a fase de carregamento inicial do
aplicativo. Especificamente, procure a fração bindApplication na
linha de execução principal e frações aninhadas que começam com madvising, que indicam o
sistema se preparando para carregar arquivos do APK e do código compilado (.odex).
Em uma inicialização a frio interativa, o sistema mmap() e madvise() código e
outros dados desses arquivos necessários para o carregamento e a execução do app. O valor depois de "size=" nas divisões madvising indica a quantidade de dados que precisa ser carregada. Esse pré-busca do código do app é feita para acelerar a inicialização
do app.
Na comparação, podemos ver que a quantidade de código do app que precisava ser
carregada do armazenamento para a RAM era muito maior no caso do app inchado, resultando
em durações mais longas que contribuíram para uma inicialização mais lenta do app. Além disso, o
rastreamento da inicialização do app inchado mostra intervalos para carregar arquivos DEX secundários
(classes2.dex, classes3.dex) que o app inchado foi forçado a "derramar" em
porque não caberia em um arquivo DEX.
Em comparação (inicialização a frio no Pixel 10a)
| Métrica | Não otimizado (CodeBloat) | Otimizado (CodeBloatOptimized) |
|---|---|---|
base.odex madvise size |
~7,9 MB (2,0 ms) | ~16 KB (0,003 ms) |
base.apk madvise size |
~2,4 MB (2,4 ms) | ~4 KB (0,001 ms) |
classes2.dex madvise size |
~7,3 MB (8,6 ms) | N/A |
classes3.dex madvise size |
~7,3 MB (8,0 ms) | N/A |
Duração total de madvising |
~21 ms | ~0,004 ms |
Desempenho de carregamento de apps não otimizado

Desempenho otimizado do carregamento de apps

O impacto do excesso de código varia de acordo com o tamanho do app, as características do dispositivo do usuário e a carga do sistema.
PerfettoSQL para análise de carregamento
É possível usar as seguintes consultas para extrair essas métricas dos seus rastreamentos.
1. Duração da inicialização do app
Isso mostra o tempo entre o lançamento de uma atividade do app e a exibição do primeiro frame.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';
Consulte: Entender os diferentes estados de inicialização do app
A duração da inicialização de um app depende de muitos fatores além dos abordados neste guia.
2. Extrair tamanhos e durações de madvising
Essa consulta detalha a parte madvising que vimos acima.
INCLUDE PERFETTO MODULE slices.with_context;
SELECT
name,
dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
AND name LIKE 'madvising %';
3. Detalhamento do estado da linha de execução principal (duração total por estado)
Essa consulta mostra quanto tempo a linha de execução principal do app passou em diferentes estados.
SELECT
p.name AS process_name,
state,
sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
GROUP BY p.name, state;
É possível refinar a consulta para analisar apenas os estados da linha de execução principal durante a duração da inicialização do app.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT
p.name AS process_name,
ts.state,
-- Calculate only the duration that falls within the startup window
SUM(
MAX(0,
MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
)
) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
-- Only select thread states that overlap with the startup interval
AND ts.ts + ts.dur > s.ts
AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;
Isso pode expor alguns problemas interessantes, por exemplo:
- Tempo gasto alto em estado Runnable (R), mas não Running: isso indica que a inicialização do app foi atrasada por disputa de CPU, ou seja, a linha de execução principal do app não pôde ser executada porque outras linhas de execução (possivelmente de outros apps) estavam ocupando as CPUs.
- Tempo alto gasto em espera interrompível (D): geralmente indica E/S lenta ou pressão de memória que está paralisando a inicialização do app.
- Tempo alto gasto em espera (S): isso significa que a linha de execução principal estava aguardando que outras linhas de execução fizessem o trabalho. Às vezes, isso indica disputa de bloqueio no caminho de inicialização do app. Ou seja, a linha de execução principal foi bloqueada em um recurso exclusivo que estava ocupado por outra linha de execução no app.
4. Memória máxima com suporte de arquivo (arquivo RSS)
Essa métrica tem boa correlação com a quantidade de código e dados que o app carrega na inicialização. Um app mais "inchado" vai atingir um número maior aqui, causando pressão de memória no sistema. Essa pressão pode atrasar a inicialização do app, já que o sistema tem dificuldade para atender às solicitações de alocação ou desvia o tempo da CPU de se concentrar na inicialização do app para recuperar a memória de outros processos e atender às necessidades imediatas do app em inicialização.
SELECT
p.name AS process_name,
max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.name = 'mem.rss.file'
GROUP BY p.name;
Modos de compilação e memória do ART
O Android Runtime (ART) pode compilar o código do aplicativo em um de vários modos diferentes, também conhecidos como filtros do compilador. O filtro de compilador selecionado tem um impacto direto no consumo de memória do app.
verify: o ART só realiza a verificação de bytecode. Nenhuma compilação AOT é realizada. O código é executado pelo Interpreter ou compilado em tempo de execução pelo compilador JIT.- Impacto na memória: menor tamanho no disco. O uso da memória do código nativo é
enviado para
JIT Cache(memória suja anônima).
- Impacto na memória: menor tamanho no disco. O uso da memória do código nativo é
enviado para
speed: o ART realiza a compilação AOT completa de todos os métodos.- Impacto na memória: maior tamanho de
.odex. Maximiza o uso da memória limpa mapeada por arquivo.
- Impacto na memória: maior tamanho de
speed-profile: a ART compila apenas métodos marcados como "ativos" em um perfil JIT.- Impacto na memória: abordagem equilibrada. Apenas o código mais crítico é compilado com AOT.
O filtro mais comum é speed-profile, usado ao instalar apps do usuário. Isso é configurado nas propriedades do sistema pm.dexopt.install e
pm.dexopt.bg-dexopt, e normalmente é definido em
build/make/target/product/runtime_libart.mk.
Alguns apps do sistema usam a compilação speed e também são compilados no tempo de build da imagem do sistema. Normalmente, verify é usado apenas em casos de uso de desenvolvimento.
| Caso de uso | Filtro de compilador típico |
|---|---|
| Desenvolvimento | verify |
| Imagem do sistema | speed |
| Apps do usuário | speed-profile |
Exercício prático: modos de compilação e memória
Podemos usar o app CodeBloat para ver como esses filtros afetam a memória. Para reproduzir essas medições:
- Força a recompilação do app no modo de destino.
- Force o fechamento e faça uma inicialização a frio do app.
- Aguarde a conclusão da thread em segundo plano (veja o logcat ou aguarde 5 segundos).
- Execute
adb shell dumpsys meminfo com.android.codebloat.
Modo: verify (sem AOT)
adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
No modo verify, o resumo do app mostra: * PSS de código: ~8.000 KB * Dalvik
Outro (JIT): ~25.000 KB
Como nenhum código é compilado AOT, o tempo de execução precisa compilar JIT métodos ativos no cache JIT, que aparece como memória anônima suja (Dalvik
Other).
Modo: speed (AOT completo)
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
No modo speed, os resultados mudam drasticamente: * PSS de código: ~24.000 KB *
Outro Dalvik (JIT): ~5.000 KB
O código do aplicativo agora é mapeado do arquivo .odex como memória limpa
protegida por arquivo. Isso reduz a pressão no cache JIT e torna a memória qualificada para remoção sob pressão, em vez de ficar "presa" como RAM suja.
Modo: speed-profile (AOT seletiva)
Os apps modernos podem agrupar um perfil de referência baseline.prof. O ART usa isso para
compilar seletivamente apenas o código necessário para uma inicialização rápida e eficiente em termos de memória.
Neste exercício, vamos criar um perfil de referência para listar as classes de inicialização do app. No entanto, na realidade, o compilador também pode receber perfis de fontes externas, como a loja de aplicativos ("perfis da nuvem"), que podem fornecer perfis JIT de crowdsourcing para apps, independente de o desenvolvedor também ter agrupado um perfil de referência gerado por ele.
Como gerar e usar perfis no dispositivo
Para conferir o impacto de speed-profile, gere seu próprio perfil no dispositivo:
Redefinir e iniciar:
adb shell am force-stop com.android.codebloatInteragir: inicie o app e deixe que ele execute a sequência de inicialização.
Despejar perfil:
adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)Isso força o app a gravar o perfil atual no disco.
Instalar perfil:
adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \ /data/misc/profiles/ref/com.android.codebloat/primary.profCompilar:
adb shell cmd package compile -m speed-profile -f com.android.codebloat
Ao iniciar novamente, você vai notar um equilíbrio: o PSS de código será menor que speed (por exemplo, ~16.000 KB) porque apenas os métodos de inicialização "ativos" foram compilados, deixando o restante para ser processado pelo interpretador ou JIT somente se forem usados.
Veja estes tópicos:
Análise detalhada do código compilado
Se você quiser saber exatamente quais instruções a ART está gerando, consulte
art/DISASSEMBLY_GUIDE.md.
Ele oferece instruções detalhadas sobre como usar:
oatdump: para conferir instruções ARM64 em um arquivo.odex.dex2oat: para simular a compilação com flags de depuração detalhadas.
Exercício: inclusão de código
Um dos motivos para o crescimento inesperado do código compilado é a inclusão de métodos. O compilador pode decidir copiar o corpo de um método pequeno e chamado com frequência diretamente para os autores da chamada.
No nosso app CodeBloat, o método doSomething() em todas as classes geradas
simplesmente chama method0(). Quando compilado no modo speed, o compilador de otimização do ART provavelmente vai inserir method0() em doSomething().
Exercício:verifique isso usando oatdump no seu dispositivo:
# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk
adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]
# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
--class-filter=com.android.codebloat.GeneratedClass0
Procure o método doSomething na saída. Se ele foi inlinado, você vai ver
as instruções para carregar a constante de string longa diretamente em doSomething,
em vez de uma instrução bl direcionada a method0.
Visualizar a otimização (CFG)
Para saber exatamente quando o compilador decidiu inserir o método inline, gere um gráfico de fluxo de controle (CFG, na sigla em inglês). Isso mostra o estado do código em todas as etapas do pipeline de otimização, com todas as transformações na representação intermediária (IR, na sigla em inglês) do compilador até que o código seja reduzido para o ISA de destino (por exemplo, ARM64).
Execute
dex2oatcom flags de despejo: use a flag--verbose-methodspara limitar a saída a métodos específicos. Caso contrário, o arquivo.cfgde um app grande pode chegar a vários gigabytes.# Substitution of actual paths required: adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \ --oat-file=/data/local/tmp/dump.odex \ --compiler-filter=speed \ --dump-cfg=/data/local/tmp/codebloat.cfg \ --verbose-methods=doSomethingExtrair e visualizar: extraia o arquivo
.cfgpara sua estação de trabalho e abra com o IR Hydra.Encontre o Inliner: no IR Hydra, carregue os artefatos de compilação e pesquise
doSomething. Compare a representação antes e depois da transmissão do Inliner. O gráfico vai se expandir à medida que as instruções demethod0forem mescladas ao caller.
Como alternativa, use a ferramenta Opt Pipeline no Compiler Explorer (conforme descrito na seção acima) e insira um código semelhante para ver uma transformação parecida realizada na transmissão Inliner.
Exercício: campos voláteis e barreiras de memória
No app MemoryLab, o campo mGarbageSink é marcado como volatile. Isso garante que o compilador não otimize nossas alocações de lixo.
public volatile byte[] mGarbageSink;
Na desmontagem do ARM64, você vai notar que cada armazenamento nesse campo é acompanhado por uma barreira de memória (dmb ish) ou pelo uso de instruções Load-Acquire/Store-Release (ldar/stlr). Isso garante a visibilidade da linha de execução, mas adiciona algumas instruções extras a cada acesso, aumentando um pouco o tamanho do código em comparação com um campo regular.
Exercício:encontre os acessos de campo e as barreiras de memória associadas na desmontagem.
Exercício: verificações implícitas de suspensão
Se você desmontar um loop, como o de generateAllocationChurn, vai notar uma instrução curiosa no final do corpo do loop:
ldr x21, [x21]
Esta é uma verificação de suspensão implícita. O ART usa isso para permitir que o coletor de lixo pause as linhas de execução com segurança. O registro x21 normalmente aponta para si mesmo.
Quando o GC precisa suspender a linha de execução, ele "corrompe" esse local de memória. Na
próxima vez que a linha de execução executar esse ldr, ela vai acionar uma falha, que o
tempo de execução captura e usa para fazer a transição da linha de execução para um estado suspenso.
Esse padrão se repete em todos os loops e no início de cada método, contribuindo para o tamanho total do código do aplicativo.
Exercício:encontre todas as verificações de suspensão implícitas na desmontagem do método e tente correlacioná-las ao código-fonte original.
← WebView | ↑ Acima | Threads →