Memory Management em JavaScript: Garbage Collector e Vazamentos de Memória
Como o JavaScript Gerencia Memória
JavaScript é uma linguagem com gerenciamento automático de memória através do Garbage Collector (GC). Diferentemente de linguagens como C ou C++, você não aloca e libera memória manualmente. O motor JavaScript (V8 no Chrome/Node.js, SpiderMonkey no Firefox) rastreia objetos em uso e remove automaticamente aqueles inacessíveis.
O conceito fundamental é reachability (acessibilidade). Um objeto é considerado "vivo" se pode ser alcançado a partir de uma raiz (variáveis globais, funções ativas, referências locais). Quando nenhuma referência aponta para um objeto, ele entra na fila de coleta. Existem dois algoritmos principais: Reference Counting (usado raramente hoje) e Mark and Sweep (padrão moderno), que marca objetos acessíveis e remove os não marcados.
// Exemplo: objetos coletáveis
function exemplo() {
let pessoa = { nome: 'João', idade: 30 };
let referencia = pessoa;
pessoa = null; // uma referência removida, mas referencia ainda existe
referencia = null; // agora SEM REFERÊNCIAS - elegível para coleta
}
exemplo();
// Após a função terminar, o objeto é coletado pelo GC
Vazamentos de Memória: Causas e Detecção
Um vazamento ocorre quando objetos permanecem na memória sem serem necessários. JavaScript não previne isso automaticamente — é responsabilidade do desenvolvedor. As causas mais comuns são referências globais acidentais, listeners não removidos, closures persistentes e temporizadores esquecidos.
O impacto é real: vazamentos crescentes degradam performance, congelam aplicações e causam crashes em long-running processes (servidores Node.js). Use as DevTools do navegador (aba Memory) para snapshots de heap e identifique padrões de crescimento. No Node.js, ferramentas como clinic.js ou node --inspect oferecem análises detalhadas.
// ❌ VAZAMENTO: referência global acidental
function criarUsuario(nome) {
usuario = { nome: nome, data: new Date() }; // 'var' omitido = global!
}
criarUsuario('Maria');
criarUsuario('Pedro');
// 'usuario' permanece na memória globalmente
// ✅ CORRETO: usar escopo apropriado
function criarUsuarioCorreto(nome) {
const usuario = { nome: nome, data: new Date() };
return usuario; // retorna e permite GC quando não mais usado
}
// ❌ VAZAMENTO: listeners não removidos
const button = document.querySelector('button');
button.addEventListener('click', () => {
console.log('Clicado');
});
// Ao remover o botão do DOM, o listener permanece!
// ✅ CORRETO: remover listeners
function setupButton() {
const button = document.querySelector('button');
const handler = () => console.log('Clicado');
button.addEventListener('click', handler);
// Quando necessário:
button.removeEventListener('click', handler);
}
Closures, Timers e Armadilhas Comuns
Closures são poderosos mas perigosos. Eles capturam o escopo da função envolvente permanentemente. Se um closure persiste indefinidamente (exemplo: callback em setInterval), todo objeto referenciado no escopo fica "preso" na memória. Temporizadores esquecidos amplificam o problema exponencialmente.
Outra armadilha clássica: arrays ou objetos crescendo indefinidamente. Caches sem limite também causam vazamentos. A solução é sempre limpar recursos: cancelar timers com clearInterval(), usar WeakMap/WeakSet para referências fracas, implementar limite de tamanho em caches.
// ❌ VAZAMENTO: setInterval sem clearInterval
function iniciarMonitor() {
let dados = [];
setInterval(() => {
dados.push(Math.random()); // dados cresce eternamente!
}, 1000);
// nunca limpa, nunca retorna handle para cancelar
}
// ✅ CORRETO: gerenciar timers
function iniciarMonitorCorreto() {
let dados = [];
const handle = setInterval(() => {
dados.push(Math.random());
if (dados.length > 100) dados.shift(); // limita tamanho
}, 1000);
return () => clearInterval(handle); // retorna função de limpeza
}
const parar = iniciarMonitorCorreto();
// Quando necessário:
parar();
// ❌ VAZAMENTO: closure com referência pesada
function criar() {
const dadosPesados = new Array(1000000).fill('x');
return () => console.log(dadosPesados.length); // closure captura array
}
const minha_funcao = criar();
// dadosPesados nunca será coletado enquanto minha_funcao existir
// ✅ CORRETO: usar WeakMap para referências fracas
const cache = new WeakMap();
function armazenarDados(chave, dados) {
cache.set(chave, dados);
// quando 'chave' for coletada, entrada do cache também será
}
Práticas de Otimização e Prevenção
A prevenção começa na arquitetura. Use padrões como Observer Pattern com limpeza automática, implemente lifecycle hooks (React.useEffect com cleanup, Angular onDestroy), e sempre destaque componentes corretamente. Em Node.js, estruture seus dados para evitar graphs circulares complexas.
Monitore regularmente. Em produção, configure alertas para vazamentos de memória em seus servidores. Use profiling durante desenvolvimento — encontrar problemas cedo é exponencialmente mais barato. Revise code reviews focando em gerenciamento de recursos, especialmente em seções críticas como integrações com APIs externas, WebSockets e long-polling.
// ✅ BOM: padrão Observable com limpeza
class EventEmitter {
constructor() {
this.listeners = [];
}
on(evento, handler) {
this.listeners.push({ evento, handler });
return () => { // retorna função de desinscrição
this.listeners = this.listeners.filter(l => l.handler !== handler);
};
}
emit(evento, dados) {
this.listeners
.filter(l => l.evento === evento)
.forEach(l => l.handler(dados));
}
}
// Uso seguro:
const emitter = new EventEmitter();
const unsubscribe = emitter.on('mensagem', (msg) => {
console.log(msg);
});
// Quando terminar:
unsubscribe();
// ✅ BOM: cache com limite de tamanho (LRU)
class CacheLimitado {
constructor(maxSize = 50) {
this.cache = new Map();
this.maxSize = maxSize;
}
set(chave, valor) {
if (this.cache.has(chave)) {
this.cache.delete(chave); // reposiciona como mais recente
}
this.cache.set(chave, valor);
if (this.cache.size > this.maxSize) {
const primeiraChave = this.cache.keys().next().value;
this.cache.delete(primeiraChave); // remove a mais antiga
}
}
get(chave) {
return this.cache.get(chave);
}
}
Conclusão
Memory management em JavaScript depende de compreender o Garbage Collector e sua mecânica de reachability. Primeiro ponto: vazamentos não são inevitáveis — resultam de práticas negligentes (listeners não removidos, timers sem clearance, closures desnecessários). Identificar e corrigir esses padrões previne 95% dos problemas reais.
Segundo ponto: use ferramentas de profiling (DevTools, clinic.js, inspector) regularmente. Problemas de memória crescem silenciosamente; monitoramento contínuo é a única defesa confiável em produção.
Terceiro ponto: adote padrões arquiteturais com limpeza explícita (padrão Observer, lifecycle hooks, funções de desinscrição). Código defensivo que antecipa vazamentos é mais profissional e mantível que debugging reativo.