Gerenciamento de memória
Pilha, heap e coletor de lixo, e de onde vêm os vazamentos de memória em linguagens gerenciadas.
As decisões de memória são quatro: como configurar o runtime para o container, como evitar vazamentos no desenho do código, como acompanhar a memória em produção e como investigar quando ela cresce.
Configurar cada runtime para o limite do container
| Linguagem | O que o runtime faz por padrão num container de 512 MB | O que configurar |
|---|---|---|
| Node | Limita o heap a cerca de metade da memória do container (259 MB no nosso teste) | --max-old-space-size para ajustar, deixando espaço para memória fora do heap |
| Python | Não tem limite próprio; usa o que precisar até o sistema operacional matar o processo | Monitorar e limitar o tamanho de caches e filas na aplicação |
| Go | Não conhece o limite; coleta quando o heap dobra em relação aos dados em uso | GOMEMLIMIT em cerca de 80 a 90% do limite do container |
| Java | Usa 25% da memória do container para o heap (123 MB no teste) | -XX:MaxRAMPercentage entre 60 e 75 |
| C# | Usa 75% da memória do container | Geralmente nada; DOTNET_GCHeapHardLimitPercent para ajustar |
Nenhuma configuração deve dar ao heap toda a memória do container. Todo processo usa memória fora do heap: o próprio runtime, as threads e as pilhas delas, buffers de rede, bibliotecas nativas, o código compilado. Deixe de 20 a 40% de folga.
Os dois lados do erro
| Configuração | Sintoma |
|---|---|
| Heap pequeno demais para o container | Erros de falta de memória do runtime com o container quase vazio, ou o coletor rodando o tempo todo |
| Heap grande demais, ou sem limite | O sistema operacional mata o processo sem mensagem (OOMKilled) |
Evitar vazamentos no desenho
| Prática | Como aplicar |
|---|---|
| Todo cache tem limite | Tamanho máximo, expiração, ou os dois; descarte do item menos usado recentemente (LRU) |
| Nada global que só cresce | Coleções globais com limite, ou trocadas por métricas e logs |
| Ouvintes com dono | Todo registro de ouvinte tem um cancelamento correspondente, de preferência no mesmo trecho de código |
| Tarefas com fim | Goroutines e tarefas assíncronas recebem um contexto ou token de cancelamento |
| Temporizadores cancelados | Todo setInterval ou temporizador tem um clear correspondente |
| Dados grandes em fluxo | Arquivos e resultados grandes são processados em partes, sem carregar tudo na memória |
O último ponto não é vazamento, mas derruba serviços do mesmo jeito: uma rota que carrega um arquivo de 1 GB ou uma consulta de um milhão de linhas inteiras na memória. Fluxos (streams), paginação e cursores do banco resolvem.
Cache em memória ou cache externo
| Cache em memória com limite | Redis ou outro cache externo | |
|---|---|---|
| Velocidade | A mais alta | Uma ida e volta pela rede |
| Memória do serviço | Ocupa memória do processo, entra na conta do container | Não ocupa |
| Várias cópias do serviço | Cada uma tem o seu, possivelmente diferente | Compartilhado |
| Risco de vazamento | Se o limite for esquecido | Controlado pela política de descarte do Redis |
O tópico de cache aprofunda a escolha. Para a memória, a regra é: se for em memória, com limite.
Acompanhar a memória em produção
| Métrica | Onde ver | Sinal de alerta |
|---|---|---|
| Memória do processo (RSS) | Métricas do container, docker stats, Kubernetes | Perto do limite do container |
| Heap usado depois de cada coleta | Métricas do runtime (V8, JVM, .NET, Go) | Subindo continuamente com carga estável |
| Frequência e duração das coletas | Métricas do runtime | Coletas cada vez mais frequentes: o heap está ficando pequeno |
| Reinícios por falta de memória | Kubernetes (OOMKilled), logs do orquestrador | Qualquer ocorrência |
O sinal mais confiável de vazamento é o heap depois da coleta crescendo ao longo de horas ou dias, sem voltar ao patamar anterior. A memória total do processo oscila naturalmente; o que sobra depois de cada coleta, não.
Investigar um vazamento
O roteiro é parecido em todas as linguagens:
- Confirmar o crescimento: o heap depois das coletas sobe com carga estável?
- Capturar o heap duas vezes, com um intervalo de tempo e de tráfego entre as capturas.
- Comparar as capturas: quais tipos de objeto cresceram em quantidade e tamanho?
- Seguir as referências: o que mantém esses objetos alcançáveis? Quase sempre é uma coleção global, um cache ou um ouvinte.
| Linguagem | Ferramenta para capturar e analisar o heap |
|---|---|
| Node | --heapsnapshot-signal=SIGUSR2 ou v8.writeHeapSnapshot(), analisado no Chrome DevTools |
| Python | tracemalloc (da biblioteca padrão), memray |
| Go | pprof com o perfil heap |
| Java | jcmd <pid> GC.heap_dump, analisado no Eclipse MAT ou no VisualVM |
| C# | dotnet-gcdump ou dotnet-dump, analisados no Visual Studio ou no PerfView |
Resumo da decisão
- Configure o runtime para o limite do container, com 20 a 40% de folga fora do heap.
- Todo cache e toda coleção global tem limite.
- Ouvintes, temporizadores e tarefas têm um fim definido.
- Monitore o heap depois das coletas e os reinícios por falta de memória.
- Investigue com duas capturas do heap, comparando o que cresceu.
O próximo nível mostra um cache em memória com limite nas cinco linguagens e as ferramentas de investigação em uso.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Configurar a memória do meu runtime
Quero configurar corretamente a memória do runtime da minha aplicação para o container onde ela roda. Pergunte qual linguagem e versão eu uso, qual é o limite de memória do container e que tipo de carga a aplicação tem. Explique, para o meu caso: 1. O que o runtime faz por padrão com esse limite. 2. Qual configuração usar e com que valor, deixando folga para memória fora do heap. 3. O que acontece se o valor for pequeno demais ou grande demais. 4. Como confirmar, depois do ajuste, que o coletor não está trabalhando demais nem deixando o processo chegar perto do limite. Termine com a linha exata de configuração para o meu Dockerfile ou manifesto.
Desenhar código que não vaza
Quero revisar as práticas de código que evitam vazamentos de memória na minha aplicação. Pergunte qual linguagem e framework eu uso e se a aplicação tem caches, filas internas, ouvintes de eventos ou tarefas em segundo plano. Para cada um desses elementos, explique: 1. Como ele pode vazar memória. 2. A forma correta de implementá-lo na minha linguagem, com código. 3. Que limite ou cancelamento ele precisa ter. Explique também como processar arquivos e resultados grandes sem carregar tudo na memória. Termine com uma lista de regras para revisão de código.
Plano de monitoramento de memória
Quero montar o monitoramento de memória da minha aplicação para detectar vazamentos antes que derrubem o serviço. Pergunte qual linguagem, plataforma de execução e ferramenta de monitoramento eu uso. Explique: 1. Quais métricas coletar: memória do processo, heap depois das coletas, frequência e duração das coletas, reinícios por falta de memória. 2. Como expor as métricas do runtime da minha linguagem. 3. Como diferenciar crescimento normal de vazamento num gráfico. 4. Quais alertas configurar, com quais limites. 5. O que fazer quando o alerta disparar.