Threads e memória

Sempre que o aplicativo cria uma nova linha de execução, o sistema operacional precisa alocar memória para ela. Embora as linhas de execução sejam geralmente consideradas "leves" em comparação com processos completos, o custo de memória delas não é zero. O vazamento de linhas de execução é uma maneira muito comum de vazar memória no Android.

O custo de uma linha de execução

Quando uma linha de execução é criada no Android, dois componentes principais consomem memória:

  1. Pilha nativa: a biblioteca pthread aloca uma pilha para a linha de execução armazenar variáveis locais e frames de chamada. No Android, o padrão é 1 MB.
  2. Estruturas internas da JVM/do kernel: o ART e o kernel do Linux precisam alocar estruturas de dados (como objetos pthread_internal_t e java.lang.Thread) para rastrear e gerenciar a linha de execução.

Memória de pilha e mmap

É importante entender que o Android aloca a pilha nativa de 1 MB usando mmap com as flags MAP_ANONYMOUS | MAP_NORESERVE.

Isso significa que o SO reserva 1 MB de espaço de endereço de memória virtual, mas não aloca 1 MB de RAM física imediatamente. A memória física só é alocada (paginada) quando a linha de execução realmente executa o código e envia dados para a pilha. Uma linha de execução que não faz nada consome pouca RAM física para a pilha.

Exercício prático: vazamento de linhas de execução

Vamos usar o aplicativo de exemplo MemoryLab para observar o que acontece quando um aplicativo cria centenas de linhas de execução inativas.

1. Lançamento e valor de referência

Inicie o app e faça uma leitura de memória de referência:

adb shell am start -W -n com.android.memorylab/.MainActivity
adb shell dumpsys meminfo -s com.android.memorylab

Observe as linhas Stack e Native Heap na saída. Elas provavelmente serão pequenas.

2. Criar 100 conversas

Toque no botão Criar 100 conversas. O aplicativo vai gerar 100 novas linhas de execução. Para garantir que a memória da pilha seja realmente paginada na RAM física (para que possamos vê-la nas nossas ferramentas), cada linha de execução aloca e grava em uma pequena matriz de 10 KB na pilha antes de começar a esperar.

Execute o comando meminfo novamente enquanto as linhas de execução estiverem ativas:

adb shell dumpsys meminfo -s com.android.memorylab

Os resultados:

Compare a nova saída com o valor de referência. Você vai notar aumentos significativos enquanto as linhas de execução estiverem ativas:

  • Stack: aumentou em um megabyte ou mais. Isso representa as páginas físicas alocadas para as 100 pilhas de linhas de execução (cada uma tocando pelo menos 10 KB).
  • Native Heap / Private Other: aumentou devido às estruturas internas de pthread alocadas pelo sistema para cada linha de execução.

Para limpar, toque no botão Destroy All Threads.

3. Como visualizar linhas de execução no Perfetto

O Perfetto é excelente para rastrear ciclos de vida e contagens de linhas de execução ao longo do tempo.

Uma captura de tela da interface do Perfetto mostrando uma faixa de contador com o número de
linhas de execução ativas e linhas de execução iniciando e saindo no app
MemoryLab

PerfettoSQL para análise de linhas de execução

É possível consultar o rastro para contar exatamente quantas linhas de execução foram geradas pelo aplicativo:

SELECT p.name AS process_name, COUNT(t.id) as thread_count
FROM thread t JOIN process p USING (upid)
WHERE p.name = 'com.android.memorylab' AND t.name LIKE 'LeakedThread-%';

Uma consulta como essa pode ser usada para gerar uma faixa de depuração com um contador, como mostrado na captura de tela.


← Código do app | ↑ Para cima | Localidade →