Gerenciamento de memória
Pilha, heap e coletor de lixo, e de onde vêm os vazamentos de memória em linguagens gerenciadas.
O vazamento mais comum em backend é o cache sem limite. Os exemplos mostram um cache em memória que guarda no máximo um número fixo de itens. Em Node, Python, Go e Java, ele descarta o item usado há mais tempo (LRU). Em C#, o exemplo usa o MemoryCache da plataforma, que limita o total de outra forma, descrita abaixo.
Conferimos o comportamento dos cinco com um limite de dois itens: guardar A e B, ler A e guardar C. Nos quatro primeiros, B foi descartado e A ficou, como se espera de um LRU.
Um cache em memória com limite
// O Map do JavaScript mantém a ordem de inserção: o primeiro item é o mais antigo
export class CacheLimitado<K, V> {
private readonly itens = new Map<K, V>();
private readonly limite: number;
constructor(limite: number) {
this.limite = limite;
}
obter(chave: K): V | undefined {
const valor = this.itens.get(chave);
if (valor !== undefined) {
this.itens.delete(chave); // move para o fim: usado agora
this.itens.set(chave, valor);
}
return valor;
}
guardar(chave: K, valor: V): void {
this.itens.delete(chave);
this.itens.set(chave, valor);
if (this.itens.size > this.limite) {
const maisAntigo = this.itens.keys().next().value as K;
this.itens.delete(maisAntigo);
}
}
}
const perfis = new CacheLimitado<number, Perfil>(10_000);from collections import OrderedDict
from functools import lru_cache
from typing import Generic, Hashable, TypeVar
C = TypeVar("C", bound=Hashable)
V = TypeVar("V")
class CacheLimitado(Generic[C, V]):
def __init__(self, limite: int):
self.limite = limite
self.itens: OrderedDict[C, V] = OrderedDict()
def obter(self, chave: C) -> V | None:
if chave not in self.itens:
return None
self.itens.move_to_end(chave) # usado agora
return self.itens[chave]
def guardar(self, chave: C, valor: V) -> None:
self.itens[chave] = valor
self.itens.move_to_end(chave)
if len(self.itens) > self.limite:
self.itens.popitem(last=False) # remove o mais antigo
perfis: CacheLimitado[int, dict] = CacheLimitado(10_000)
# Para funções, a biblioteca padrão já tem: sempre com maxsize
@lru_cache(maxsize=1024)
def cotacao(moeda: str) -> float: ...package cache
import (
"container/list"
"sync"
)
type entrada[K comparable, V any] struct {
chave K
valor V
}
// CacheLimitado é seguro para uso por várias goroutines.
type CacheLimitado[K comparable, V any] struct {
mu sync.Mutex
limite int
ordem *list.List // frente: mais recente; fundo: mais antigo
itens map[K]*list.Element
}
func Novo[K comparable, V any](limite int) *CacheLimitado[K, V] {
return &CacheLimitado[K, V]{limite: limite, ordem: list.New(), itens: map[K]*list.Element{}}
}
func (c *CacheLimitado[K, V]) Obter(chave K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if el, ok := c.itens[chave]; ok {
c.ordem.MoveToFront(el)
return el.Value.(*entrada[K, V]).valor, true
}
var zero V
return zero, false
}
func (c *CacheLimitado[K, V]) Guardar(chave K, valor V) {
c.mu.Lock()
defer c.mu.Unlock()
if el, ok := c.itens[chave]; ok {
el.Value.(*entrada[K, V]).valor = valor
c.ordem.MoveToFront(el)
return
}
c.itens[chave] = c.ordem.PushFront(&entrada[K, V]{chave, valor})
if c.ordem.Len() > c.limite {
maisAntigo := c.ordem.Back()
c.ordem.Remove(maisAntigo)
delete(c.itens, maisAntigo.Value.(*entrada[K, V]).chave)
}
}import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public final class CacheLimitado {
// LinkedHashMap com ordem de acesso remove o item usado há mais tempo
public static <K, V> Map<K, V> criar(int limite) {
var mapa = new LinkedHashMap<K, V>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> maisAntigo) {
return size() > limite;
}
};
return Collections.synchronizedMap(mapa); // seguro entre threads
}
}
// Uso:
// Map<Long, Perfil> perfis = CacheLimitado.criar(10_000);
// Em produção, a biblioteca Caffeine oferece o mesmo com expiração e melhor desempenho.using Microsoft.Extensions.Caching.Memory;
// O MemoryCache com SizeLimit exige que cada item declare o seu tamanho.
// Quando cheio, ele recusa itens novos e compacta em segundo plano: não é um LRU.
var perfis = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 10_000, // aqui, 1 unidade = 1 item
});
void Guardar(long usuarioId, Perfil perfil) =>
perfis.Set(usuarioId, perfil, new MemoryCacheEntryOptions
{
Size = 1,
SlidingExpiration = TimeSpan.FromMinutes(10),
});
Perfil? Obter(long usuarioId) =>
perfis.TryGetValue(usuarioId, out Perfil? perfil) ? perfil : null;
public record Perfil(long Id, string Nome);O que muda de uma linguagem para outra
| Estrutura usada | Seguro entre threads | Alternativa pronta | |
|---|---|---|---|
| Node | Map, que guarda a ordem de inserção | Não precisa: uma thread | lru-cache |
| Python | OrderedDict; lru_cache para funções | Não, sem trava própria | cachetools.LRUCache, TTLCache |
| Go | container/list com um mapa | Sim, com sync.Mutex | hashicorp/golang-lru |
| Java | LinkedHashMap com ordem de acesso | Sim, com synchronizedMap | Caffeine |
| C# | MemoryCache com SizeLimit | Sim | O HybridCache, ou um LRU próprio com LinkedList e Dictionary |
Três detalhes que costumam passar despercebidos:
lru_cachesemmaxsize, ou commaxsize=None, é um cache sem limite. A anotação parece segura e não é. O mesmo vale parafunctools.cache, que nunca descarta nada.MemoryCachesemSizeLimitnão limita nada. ComSizeLimitdefinido, todo item precisa informar umSize: no nosso teste, umSetsemSizelançouInvalidOperationException.MemoryCachecheio recusa o item novo. No mesmo teste, com o limite de dois itens ocupado por A e B, o C não foi guardado, e A e B continuaram. A memória fica limitada, que é o que evita o vazamento, mas os itens novos e mais procurados podem ficar de fora até a compactação, que remove itens vencidos e de menor prioridade em segundo plano. Use expiração (SlidingExpirationouAbsoluteExpirationRelativeToNow) junto com o limite, para que o cache se renove.
Limitar pela quantidade de itens só funciona se os itens têm tamanho parecido. Se alguns forem muito maiores que outros, limite pelo tamanho estimado em bytes, como o MemoryCache e o lru-cache permitem.
Configurar o runtime: os testes deste tópico
Os resultados do nível Conceito vieram destes comandos, num container de 512 MB:
# Go: 300 MB em uso e 3 GB de lixo temporário
docker run --memory=512m --memory-swap=512m minha-imagem-go
# → morto pelo sistema operacional (código 137, OOMKilled)
docker run --memory=512m --memory-swap=512m -e GOMEMLIMIT=450MiB minha-imagem-go
# → terminou normalmente, com 30 coletas
# Java: 250 MB em uso
docker run --memory=512m eclipse-temurin:21-jdk java Memoria.java
# → java.lang.OutOfMemoryError: Java heap space (heap padrão de 123 MB)
docker run --memory=512m eclipse-temurin:21-jdk java -XX:MaxRAMPercentage=75 Memoria.java
# → funcionou, com heap máximo de 371 MB--memory-swap igual a --memory desliga o uso de disco como memória extra, que esconderia o problema atrás de uma lentidão enorme. Em produção, os orquestradores costumam se comportar da mesma forma.
Para conferir se um container foi morto por falta de memória:
docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' nome-do-container
# 137 true → morto pelo sistema operacional por exceder o limite
kubectl describe pod nome-do-pod | grep -A2 "Last State"
# Reason: OOMKilledEncontrar o que está ocupando a memória
Um exemplo com o tracemalloc, da biblioteca padrão do Python, comparando duas fotografias da memória:
import tracemalloc
tracemalloc.start()
antes = tracemalloc.take_snapshot()
processar_lote_de_pedidos() # o trecho suspeito
depois = tracemalloc.take_snapshot()
for diferenca in depois.compare_to(antes, "lineno")[:5]:
print(diferenca)
# app/cache.py:42: size=48.2 MiB (+48.2 MiB), count=50012 (+50012)A saída aponta o arquivo e a linha onde a memória que cresceu foi reservada. Em Go, o equivalente é o pprof:
# Com import _ "net/http/pprof" e um servidor de depuração na porta 6060
go tool pprof -top http://localhost:6060/debug/pprof/heapE no Node, uma captura do heap enviada por sinal, sem mudar o código:
node --heapsnapshot-signal=SIGUSR2 servidor.js
kill -USR2 <pid> # grava um arquivo .heapsnapshot no diretório atualO arquivo abre na aba Memory do Chrome DevTools. Duas capturas comparadas pela visão Comparison mostram quais tipos de objeto cresceram entre elas.
Lista de revisão
Código
- Todo cache em memória tem limite de tamanho ou de itens, e de preferência expiração?
lru_cachetemmaxsizedefinido?MemoryCachetemSizeLimit?- Nenhuma coleção global só cresce?
- Ouvintes, temporizadores e tarefas têm cancelamento?
- Arquivos e resultados grandes são processados em partes?
Configuração
- O runtime conhece o limite do container:
GOMEMLIMIT,MaxRAMPercentage,max-old-space-size? - Há folga de 20 a 40% para memória fora do heap?
Operação
- O heap depois das coletas é monitorado ao longo do tempo?
- Reinícios por falta de memória geram alerta?
- A equipe sabe capturar e comparar o heap da linguagem usada?
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.
Reproduzir e consertar um vazamento
Quero reproduzir um vazamento de memória, encontrá-lo com as ferramentas da minha linguagem e consertá-lo. Pergunte qual linguagem eu uso. Vamos em etapas: 1. Escreva comigo um servidor com um cache global sem limite, que guarda dados de cada usuário novo. 2. Rode num container com limite de memória e gere carga até ele morrer. Observe se foi o runtime ou o sistema operacional. 3. Capture o heap duas vezes durante a carga e compare as capturas com a ferramenta da linguagem. 4. Encontre na comparação o objeto que cresce e o que o mantém alcançável. 5. Troque por um cache com limite e confirme que a memória se estabiliza. Em cada etapa, me diga o que observar.
Ver o efeito do limite de memória no coletor
Quero ver na prática como o limite de memória do container afeta o coletor de lixo da minha linguagem. Pergunte qual linguagem eu uso. Vamos em etapas: 1. Escreva um programa que mantém uma quantidade fixa de dados em uso e cria muitos objetos temporários. 2. Rode num container com limite de memória e swap desligado, com a configuração padrão do runtime. 3. Repita com a configuração recomendada (GOMEMLIMIT, MaxRAMPercentage, max-old-space-size ou equivalente). 4. Compare o resultado, o número de coletas e o tempo total. 5. Explique qual valor eu deveria usar em produção e por quê.
Revisar código e configuração de memória
Vou colar abaixo código e configuração de implantação de um serviço. Revise com foco em memória. Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho: 1. Há caches em memória sem limite de tamanho ou expiração? 2. Há coleções globais que só crescem? 3. Há ouvintes, temporizadores ou tarefas sem cancelamento? 4. Há leitura de arquivos ou resultados grandes inteiros na memória? 5. O runtime está configurado para o limite de memória do container, com folga? 6. O limite de memória do container existe e é coerente com a carga? Para cada problema, descreva como ele se manifesta em produção e mostre a correção. Não reescreva o que já está correto. Código e configuração: [cole aqui]