Gerenciamento de memória
Pilha, heap e coletor de lixo, e de onde vêm os vazamentos de memória em linguagens gerenciadas.
Onde os dados moram: pilha e heap
Um programa guarda os seus dados em duas regiões de memória com regras diferentes:
| Pilha (stack) | Heap | |
|---|---|---|
| O que guarda | Variáveis locais e dados de cada chamada de função em andamento | Objetos que vivem além da função que os criou: listas, mapas, objetos, textos longos |
| Como é liberada | Automaticamente, quando a função termina | Por alguém: o programador, ou o coletor de lixo |
| Velocidade | Muito rápida | Mais lenta para reservar e liberar |
| Tamanho | Pequena, alguns megabytes por thread | Grande, limitada pela memória disponível |
Quase tudo o que importa num backend mora no heap: os pedidos recebidos, as respostas montadas, os resultados das consultas, os caches. É no heap que a memória acaba.
Quem libera a memória
Em C e C++, o programador reserva e libera a memória à mão. Esquecer de liberar vaza memória; liberar duas vezes ou usar algo já liberado corrompe o programa e abre falhas de segurança.
As cinco linguagens desta trilha usam um coletor de lixo (garbage collector): o runtime descobre sozinho quais objetos não são mais usados e libera a memória deles.
| Linguagem | Como o coletor funciona |
|---|---|
| Node (V8) | Por geração: objetos novos são coletados com frequência; os que sobrevivem passam para uma área coletada com menos frequência |
| Python (CPython) | Contagem de referências: cada objeto sabe quantas referências tem e é liberado quando chega a zero; um coletor extra trata ciclos |
| Go | Concorrente: marca e libera enquanto o programa roda, com pausas muito curtas; sem gerações |
| Java | Por geração, com vários coletores para escolher (G1 é o padrão em servidores; ZGC para pausas mínimas) |
| C# (.NET) | Por geração, com modo de estação de trabalho e modo de servidor |
A regra do coletor: o que é alcançável fica
O coletor não sabe se um objeto ainda vai ser usado. Ele sabe se o objeto ainda é alcançável: se existe um caminho de referências partindo das variáveis globais, das variáveis locais em uso ou de outras raízes até ele.
raízes (variáveis globais, pilhas das threads)
│
├──▶ cache global ──▶ usuário 1 ──▶ perfil
│ ──▶ usuário 2 ──▶ perfil ← alcançáveis: ficam
│
└──▶ pedido atual ──▶ resposta
objeto de um pedido que já terminou ← inalcançável: é coletadoIsso explica a forma dos vazamentos em linguagens com coletor: uma referência esquecida num lugar que dura para sempre. O objeto nunca mais será usado, mas um mapa global ainda aponta para ele, e o coletor é obrigado a mantê-lo.
De onde vêm os vazamentos
| Causa | Exemplo |
|---|---|
| Cache sem limite | Um mapa global que guarda o perfil de cada usuário que já fez um pedido |
| Coleções globais que só crescem | Uma lista de pedidos processados guardada "para depuração" |
| Ouvintes não removidos | Um ouvinte de eventos registrado a cada pedido e nunca desregistrado |
| Closures que capturam demais | Uma função guardada que referencia, sem precisar, um objeto grande do contexto em que foi criada |
| Tarefas que nunca terminam | Goroutines ou tarefas esperando um canal ou um evento que nunca chega |
| Temporizadores esquecidos | Um setInterval criado por pedido e nunca cancelado |
| Valores por thread | ThreadLocal em Java preso a threads de um pool, que nunca morrem |
O mais comum é o primeiro. Medimos com um cache em Node que guardava o perfil de cada usuário novo, num container de 512 MB:
| Cache | Depois de 400 mil pedidos | Depois de 2 milhões de pedidos |
|---|---|---|
| Limitado a 10 mil itens, removendo o mais antigo | 15 MB de heap | 12 a 18 MB de heap |
| Sem limite | 156 MB de heap | O processo morreu com JavaScript heap out of memory antes de 800 mil |
Com tráfego real, o cache sem limite leva dias para encher, e o serviço reinicia sozinho de tempos em tempos. Sem a medição, a causa parece aleatória.
Quando a memória acaba: duas mortes diferentes
| Quem desiste | O que acontece | Como aparece |
|---|---|---|
| O runtime | O heap chegou ao limite configurado do runtime | Um erro com mensagem: JavaScript heap out of memory, OutOfMemoryError, MemoryError |
| O sistema operacional | O processo inteiro passou do limite do container | O processo é morto sem mensagem nenhuma; código de saída 137 e o motivo OOMKilled |
A segunda é a mais confusa, porque não deixa rastro no log da aplicação. E ela acontece quando o runtime não sabe que existe um limite.
O coletor precisa conhecer o limite
Um coletor de lixo decide quando coletar com base em quanto o heap cresceu. Se ele não sabe que o container tem 512 MB, pode deixar o lixo acumular além disso antes de coletar.
Reproduzimos com um programa Go que mantém 300 MB de dados em uso e cria 3 GB de objetos temporários, num container de 512 MB:
| Configuração | Resultado |
|---|---|
| Padrão do Go, sem limite de memória | Morto pelo sistema operacional (código 137, OOMKilled) |
Com GOMEMLIMIT=450MiB | Terminou normalmente, com 30 coletas |
Os dados em uso cabiam com folga. O problema era o lixo: por padrão, o Go deixa o heap crescer até o dobro dos dados em uso antes de coletar, e o dobro de 300 MB passa de 512. Com o limite informado, o coletor passou a trabalhar mais cedo.
O Java mostra o problema oposto. No mesmo container, um programa que precisava de 250 MB falhou com OutOfMemoryError: por padrão, a JVM usa só 25% da memória do container para o heap, cerca de 123 MB. Com -XX:MaxRAMPercentage=75, o heap passou a 371 MB e o programa funcionou.
Por que isso importa quando a IA escreve o código
Assistentes escrevem caches, registros e coleções sem pensar em quanto eles crescem em produção:
- Um
Mapou dicionário global usado como cache, sem limite de tamanho nem expiração. - Um ouvinte de eventos registrado dentro de uma função chamada a cada pedido.
- Uma lista que acumula resultados "para estatísticas" e nunca é esvaziada.
- Goroutines disparadas sem forma de terminar quando o pedido é cancelado.
- O container configurado com um limite de memória e o runtime deixado no padrão.
O próximo nível compara as formas de configurar cada runtime e de evitar e encontrar vazamentos.
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.
Memória e coletor de lixo em cinco minutos
Explique em no máximo cinco minutos de leitura como funciona o gerenciamento de memória em linguagens com coletor de lixo, para alguém que programa mas nunca investigou um problema de memória. Cubra só o essencial: a diferença entre pilha e heap, o que o coletor de lixo faz, por que ainda existem vazamentos e o que acontece quando um container fica sem memória. Use uma analogia simples. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Como o coletor de lixo decide o que liberar
Quero entender como um coletor de lixo decide o que liberar. Aja como um mentor. Pergunte primeiro em qual linguagem eu trabalho. Conduza assim, uma etapa por vez: 1. A diferença entre pilha e heap, com um exemplo de código que coloca dados em cada uma. 2. O conceito de raízes e de objetos alcançáveis, com um desenho em texto. 3. Como funciona o coletor da minha linguagem: por geração, contagem de referências ou concorrente. 4. Por que um objeto que nunca mais será usado pode continuar na memória para sempre. 5. O que é uma pausa de coleta e quando ela afeta a latência. Faça uma pergunta de verificação em cada etapa e espere a minha resposta.
Encontrar o vazamento escondido
Quero treinar o olho para reconhecer vazamentos de memória em linguagens com coletor de lixo. Pergunte em qual linguagem eu trabalho. Me mostre oito trechos de código, um por vez, e pergunte se cada um vaza memória e por quê. Inclua: - um cache global sem limite; - um ouvinte de eventos registrado a cada pedido; - uma closure que captura um objeto grande sem precisar; - uma tarefa ou goroutine que espera algo que nunca chega; - um temporizador criado por pedido; - uma lista global "para estatísticas"; - dois trechos corretos. Corrija cada resposta e mostre a versão sem vazamento.
Por que meu container é morto por falta de memória
Quero entender as duas formas de um serviço morrer por falta de memória e como cada runtime se comporta num container. Explique: 1. A diferença entre o runtime lançar um erro de falta de memória e o sistema operacional matar o processo (OOMKilled, código 137). 2. Como o coletor decide quando coletar, e por que um coletor que não conhece o limite do container pode deixar o lixo passar do limite. 3. O comportamento padrão de Node, Python, Go, Java e C# num container com limite de memória. 4. Por que memória alta depois de um pico não é necessariamente vazamento. Depois me descreva três situações de produção e pergunte qual é a causa provável em cada uma. Corrija as minhas respostas.