Magia que você é obrigado a entender
\n\nTodo mundo que escreveu em C# por pelo menos algumas semanas sabe: a memória é gerenciada pelo ambiente de execução (CLR). Você simplesmente cria objetos com new, e quando eles não são mais necessários — o coletor de lixo (GC) vem e limpa tudo. Parece mágica, certo? Mas por trás dessa mágica existe uma engenharia rigorosa, e se você não a entender — sua aplicação vai começar a "travar", "consumir" gigabytes de RAM ou até cair com OutOfMemoryException.
Neste artigo, vamos olhar sob o capô da CLR e entender exatamente como funciona o gerenciamento de memória em C#. Você aprenderá a diferença entre pilha e heap, o que são gerações de objetos, por que IDisposable é seu melhor amigo e como não pisar no ralo com o Large Object Heap. Vamos nessa!
Pilha e heap: o fundamento que é omitido nos livros didáticos
\n\nQuando seu programa está em execução, a CLR divide a memória em duas áreas principais: pilha (Stack) e heap (Heap). Entender a diferença entre elas é a base, sem a qual todo o resto perde o sentido.
\n\nPilha — rápida, mas pequena
\nA pilha é a área de memória onde são armazenados value types (int, bool, struct, enum) e referências para objetos. Ela funciona pelo princípio LIFO (Last In, First Out). Quando você chama um método, um frame é alocado na pilha para ele. Assim que o método termina — o frame é destruído instantaneamente. Nenhum coletor de lixo é necessário aqui. A velocidade de acesso à pilha é máxima, mas o tamanho é limitado (geralmente 1-4 MB por thread).
\n\nvoid Example()\n{\n int x = 42; // x está na pilha\n string s = "Olá"; // referência s na pilha, o objeto em si — no heap\n}\n\n\nHeap — mais lento, mas mais espaçoso
\nO heap (Managed Heap) é a memória "de longo prazo". Aqui vivem todos os reference types (classes, strings, arrays, delegates). A alocação de memória no heap ocorre a cada new. E é aqui que o coletor de lixo entra em ação. É ele quem decide quando a memória pode ser liberada e quando deve esperar.
Importante: A pilha não é "ruim", e o heap não é "bom". Cada um tem sua função. Value types são rápidos, mas não podem ser passados por referência sem a palavra-chave ref. Reference types são flexíveis, mas exigem o GC.
Como funciona o coletor de lixo em C#: gerações e algoritmo
\n\nO coletor de lixo no .NET não é apenas "removeu e esqueceu". É um mecanismo inteligente que usa gerações (Generations) para minimizar pausas na execução da aplicação.
\n\nTrês gerações de objetos
\n- \n
- Gen 0 — objetos recém-nascidos. Vivem pouco tempo. A coleta da Gen 0 ocorre com frequência e é muito rápida. \n
- Gen 1 — "sobreviventes" após uma coleta. Zona de buffer entre objetos jovens e antigos. \n
- Gen 2 — objetos de longa duração. A coleta da Gen 2 é a mais cara e rara. Se um objeto chegou à Gen 2, provavelmente permanecerá lá por muito tempo. \n
O algoritmo de funcionamento do GC, de forma simplificada, é assim:
\n- \n
- A aplicação solicita memória. Se não houver espaço no heap — a coleta é iniciada. \n
- O GC constrói um grafo de acessibilidade de objetos, começando pelas raízes (campos estáticos globais, variáveis locais nas pilhas das threads, registradores da CPU). \n
- Todos os objetos inacessíveis são marcados como "lixo". \n
- Ocorre a compactação (compaction) — objetos vivos são deslocados para evitar fragmentação. \n
- A memória liberada retorna ao pool. \n
Aqui está um exemplo claro de como o GC afeta o desempenho:
\n\nclass HeavyObject\n{\n public byte[] Data = new byte[1024 * 1024]; // 1 MB\n}\n\nvoid TestGC()\n{\n for (int i = 0; i < 1000; i++)\n {\n var obj = new HeavyObject(); // Cada vez alocamos 1 MB no heap\n // obj sai do escopo — vira lixo\n }\n // Após o loop, o GC pode coletar a Gen 0 não