Memória e garbage collection
LidoNesta aula, exploramos como o JavaScript gerencia memória automaticamente por meio do garbage collector, identificamos vazamentos de memória comuns e aprendemos técnicas para investigar problemas de memória usando ferramentas do navegador.
Nesta aula, vamos mergulhar no funcionamento da memória em JavaScript, focando no garbage collector (GC) e nos problemas que podem surgir quando a memória não é gerida corretamente. Entender como o GC opera é essencial para escrever código eficiente e evitar vazamentos que degradam a performance da aplicação.
O JavaScript é uma linguagem com gerenciamento de memória automático: o desenvolvedor não precisa alocar ou liberar memória manualmente. No entanto, isso não significa que podemos ignorar a memória; decisões de design podem levar a retenções indesejadas que sobrecarregam o GC e causam lentidão. Vamos aprender a reconhecer esses cenários e a investigar problemas de memória com ferramentas práticas.
Como o GC funciona
O garbage collector (GC) é o componente responsável por liberar memória que não é mais utilizada pelo programa. Em JavaScript, a memória é alocada para objetos, strings, closures e outras estruturas. O GC identifica quais objetos estão inacessíveis a partir das raízes (como variáveis globais, pilha de execução e referências de escopo) e os remove, liberando espaço.
O mecanismo mais comum é a marcação e varredura (mark-and-sweep). O GC percorre o grafo de objetos a partir das raízes, marcando todos os objetos alcançáveis. Depois, varre a memória e desaloca os objetos não marcados. Esse processo ocorre em ciclos, e o timing é controlado pelo runtime (V8 no Chrome/Node.js, SpiderMonkey no Firefox, etc.).
Um conceito importante é a geração: objetos recém-criados são colocados em uma geração jovem, que é coletada com mais frequência. Objetos que sobrevivem a várias coletas são promovidos para a geração antiga, coletada com menos frequência. Isso otimiza o desempenho, pois a maioria dos objetos tem vida curta.
Vamos ver um exemplo simples de como referências mantêm objetos vivos:
let objeto = { nome: 'exemplo' };
let copia = objeto; // 'copia' referencia o mesmo objeto
objeto = null; // 'objeto' não referencia mais, mas 'copia' ainda mantém vivo
console.log(copia.nome); // 'exemplo' - o objeto não foi coletado
Aqui, mesmo após definir objeto = null, o objeto continua acessível por meio da variável copia, então não é coletado. O GC só libera a memória quando não há mais referências alcançáveis.
Vazamentos comuns
Um vazamento de memória ocorre quando objetos não são mais necessários, mas ainda são referenciados, impedindo que o GC os libere. Isso pode levar ao aumento contínuo do uso de memória e, eventualmente, a travamentos. Vamos listar os vazamentos mais comuns em JavaScript.
Variáveis globais acidentais: Atribuir um valor a uma variável sem declará-la (sem let, const ou var) cria uma propriedade no objeto global (window no navegador). Isso mantém a referência enquanto a página estiver aberta.
function criarVazamento() {
vazamento = { dados: new Array(1000000) }; // sem declaração
}
criarVazamento();
// 'vazamento' agora é uma propriedade de window, nunca será coletado
Closures não intencionais: Closures capturam o escopo externo, mantendo referências a variáveis que podem ser grandes. Se uma closure é armazenada em um lugar de longa duração, ela mantém o escopo vivo.
function criarClosure() {
const dadosGrandes = new Array(1000000);
return function() {
console.log(dadosGrandes.length);
};
}
const funcao = criarClosure();
// 'dadosGrandes' fica retido enquanto 'funcao' existir
Timers e listeners não limpos: setInterval, setTimeout e event listeners que não são removidos mantêm referências a objetos. Em SPAs, é comum adicionar listeners sem removê-los ao destruir componentes.
function iniciarTimer() {
const dados = { /* ... */ };
setInterval(() => {
// usa 'dados'
}, 1000);
}
iniciarTimer();
// O timer nunca é limpo, mantendo 'dados' vivo para sempre
Referências a DOM desconectado: Quando um elemento é removido do DOM, mas ainda há referências JavaScript a ele, ele permanece na memória. Isso é comum em bibliotecas de manipulação de DOM.
const elemento = document.getElementById('meu-elemento');
document.body.removeChild(elemento);
// 'elemento' ainda referencia o nó removido, impedindo a coleta
Caches sem limite: Mapas ou arrays usados como cache podem crescer indefinidamente se não houver política de remoção.
const cache = new Map();
function processar(id) {
if (!cache.has(id)) {
cache.set(id, { dados: /* ... */ });
}
return cache.get(id);
}
// Se 'processar' é chamado com muitos ids diferentes, o cache cresce sem limite
Como investigar
Para identificar vazamentos de memória, precisamos usar ferramentas de profiling. O Chrome DevTools oferece o Memory panel, que permite tirar snapshots de heap, gravar atividades e analisar retenção de objetos.
Gravando e comparando snapshots: Tire um snapshot inicial, execute ações que suspeita causar vazamento, tire outro snapshot e compare. Objetos que aparecem no segundo snapshot e não no primeiro podem ser vazamentos.
// Exemplo de código que pode vazar
const itens = [];
function adicionarItem() {
itens.push({ dados: new Array(10000) });
}
// Chame adicionarItem() várias vezes e observe o heap
Usando a gravação de alocações: No DevTools, a opção "Record allocation timeline" mostra onde as alocações ocorrem ao longo do tempo. Isso ajuda a identificar picos de memória e funções que alocam demais.
Analisando a árvore de retenção: Ao inspecionar um objeto em um snapshot, você pode ver a cadeia de referências que o mantém vivo. Isso revela a causa raiz do vazamento.
Ferramentas no Node.js: O Node.js oferece o módulo v8 e a flag --inspect para usar o DevTools. Além disso, o process.memoryUsage() fornece estatísticas de memória.
console.log(process.memoryUsage());
// { rss, heapTotal, heapUsed, external, arrayBuffers }
Uso de WeakMap e WeakSet: Essas estruturas permitem que o GC colete chaves se não houver outras referências. São úteis para caches sem vazar.
const cache = new WeakMap();
function processar(objeto) {
if (!cache.has(objeto)) {
cache.set(objeto, { resultado: /* ... */ });
}
return cache.get(objeto);
}
// Quando 'objeto' não for mais referenciado, a entrada no WeakMap pode ser coletada
Boas práticas
- Sempre declare variáveis com
let,constouvarpara evitar globais acidentais. - Limpe timers e listeners quando não forem mais necessários, especialmente em componentes de UI.
- Evite caches sem limite; use
WeakMapou implemente políticas de remoção (LRU). - Remova referências a elementos DOM após removê-los da árvore.
- Use closures com cuidado: evite capturar variáveis grandes desnecessariamente.
Exercícios
- Explique por que o código a seguir pode causar vazamento de memória e como corrigi-lo:
function iniciar() { const dados = { nome: 'teste' }; setInterval(() => { console.log(dados.nome); }, 1000); } iniciar();✓ Resposta: OsetIntervalmantém a closure que capturadadosvivo para sempre, pois o timer nunca é limpo. Para corrigir, devemos limpar o timer quando não for mais necessário:let timer; function iniciar() { const dados = { nome: 'teste' }; timer = setInterval(() => { console.log(dados.nome); }, 1000); } // Em algum momento, chame clearInterval(timer); - Considere o código a seguir. Qual vazamento potencial existe? Como usar WeakMap para evitar?
const cache = new Map(); function buscar(objeto) { if (!cache.has(objeto)) { cache.set(objeto, { resultado: 'valor' }); } return cache.get(objeto); }✓ Resposta: OMapmantém referências fortes às chaves, então os objetos nunca serão coletados, mesmo se não forem mais usados. Trocar porWeakMappermite que o GC colete as chaves quando não houver outras referências:const cache = new WeakMap(); function buscar(objeto) { if (!cache.has(objeto)) { cache.set(objeto, { resultado: 'valor' }); } return cache.get(objeto); } - Escreva um trecho de código que remova corretamente um event listener para evitar vazamento.✓ Resposta: Para remover um listener, é necessário manter a referência à função:
const botao = document.getElementById('meu-botao'); function aoClicar() { console.log('Clicado'); } botao.addEventListener('click', aoClicar); // Em algum momento, remover: botao.removeEventListener('click', aoClicar); - O que a ferramenta "Heap Snapshot" no Chrome DevTools faz e como ela ajuda a investigar vazamentos?✓ Resposta: O Heap Snapshot captura uma imagem da memória heap em um determinado momento, listando todos os objetos e suas referências. Ao comparar snapshots antes e depois de uma ação, podemos identificar objetos que foram alocados e não liberados, e analisar a árvore de retenção para descobrir o que os mantém vivos.
- Dê um exemplo de vazamento por variável global acidental e como evitá-lo.✓ Resposta: Exemplo:
A variávelfunction criar() { dados = { grande: new Array(1000000) }; // sem declaração }dadosvira uma propriedade dewindowe nunca é coletada. Para evitar, declare comletouconst:function criar() { const dados = { grande: new Array(1000000) }; }