Trilha 06 · Memória e execução

Coleta de lixo

Python, Java, Go e JavaScript não exigem que você chame free(). Um coletor de lixo (garbage collector) rastreia automaticamente quais objetos ainda são alcançáveis e libera os demais. Mas não é mágica — há custo, pausa e algoritmos.

① Intuição

Lixo = objeto que nenhum código pode mais acessar

Um objeto é "lixo" quando não existe mais nenhum caminho a partir de uma variável ativa que chegue até ele. O programa não pode usá-lo, mas ele ainda ocupa memória. O trabalho do GC é identificar esses objetos e liberar o espaço.

O modelo mental é um grafo: os nós são objetos no heap, as arestas são referências entre eles. As raízes são os pontos de entrada — variáveis globais e frames de função ativos na stack. Um objeto é alcançável se existe um caminho das raízes até ele. Tudo que for inalcançável é lixo.

GC não é gratuito: o coletor precisa parar o programa para examinar o heap com segurança (stop-the-world), ou usar barreiras de memória para rastrear mudanças enquanto o programa roda (GC concorrente). Em aplicações de tempo real (jogos, sistemas de trading), pausas de GC de poucos milissegundos podem ser inaceitáveis — daí linguagens sem GC (C, C++, Rust) ou com GC de baixa pausa (Go, JVM com ZGC).
② Visualização interativa

Mark-and-Sweep ao vivo

Clique em "Marcar" para ver o GC fazer DFS a partir das raízes, colorindo objetos alcançáveis (verde) e inalcançáveis (vermelho). Depois "Varrer" para coletar os inalcançáveis.

■ Raiz (root)■ Objeto comum
ArootBrootCDEFGH
Grafo de objetos em heap. Raízes (roots) são pontos de partida — variáveis globais e frames de stack ativos. G e H não têm caminho a partir das raízes.
③ Explicação técnica

Mark-and-Sweep

// Mark-and-Sweep: o algoritmo GC clássico

// 1. PAUSA o programa ("stop-the-world")
// 2. MARCA: DFS/BFS a partir das raízes
//    Raízes = variáveis globais + variáveis nos stack frames ativos

def mark(obj):
    if obj.marcado: return
    obj.marcado = True
    for ref in obj.referencias:
        mark(ref)    // percorre todo o grafo de objetos

for raiz in todas_as_raizes:
    mark(raiz)

// 3. VARRE: percorre o heap e coleta os NÃO marcados
for obj in todo_o_heap:
    if not obj.marcado:
        liberar(obj)   // inalcançável → lixo

// 4. RETOMA o programa

Contagem de referências

// Contagem de referências: alternativa ao mark-and-sweep
// Cada objeto mantém um contador: quantas referências apontam para ele?

a = Obj()    // rc(a) = 1
b = a        // rc(a) = 2
b = None     // rc(a) = 1
a = None     // rc(a) = 0 → liberado IMEDIATAMENTE

// Vantagem: sem "stop-the-world", liberação determinística
// Desvantagem fatal: ciclos nunca chegam a rc = 0

a = Obj(); b = Obj()
a.outro = b; b.outro = a     // ciclo!
a = None; b = None
// rc(a_obj) = 1 (b_obj aponta), rc(b_obj) = 1 (a_obj aponta)
// Nenhum chega a 0 → leak permanente

// Solução: GC cíclico adicional (CPython faz isso)
// Swift/Rust: weak references quebram ciclos manualmente
GC geracional: a maioria dos objetos morre jovem (hipótese geracional). JVM, .NET e CPython dividem o heap em gerações: objetos novos ficam na geração 0 (pequena, coletada frequentemente). Sobreviventes sobem para gerações mais velhas (coletadas raramente). Assim, o GC rastreia apenas uma fração pequena do heap na maioria das coletas — pausas menores, throughput maior.
④ Projeto para programar

Implementando um GC simples

Mini projeto: em Python, simule um grafo de objetos com um dicionário de listas de adjacência. Implemente mark_and_sweep(roots, graph) que retorna o conjunto de nós coletados. Teste com casos que incluam ciclos e confirme que mark-and-sweep os coleta enquanto contagem de referências falha.

Projeto principal: em C, implemente um GC Cheney (cópia de objetos) simplificado: dois semiespaços de memória. Durante a coleta, todos os objetos alcançáveis são copiados para o outro semiespaço (compactando o heap no processo). O semiespaço antigo é descartado inteiro. Meça a velocidade de alocação vs. mark-and-sweep.

Desafio extra: estude o código-fonte do GC do CPython (Modules/gc.c) ou do Go (runtime/mgc.go). Documente em um texto curto: qual algoritmo é usado, como ciclos são detectados, e como a implementação gerencia as pausas. Qual é a principal troca (tradeoff) de cada abordagem?

⑤ Exercícios rápidos

Teste sua intuição

O que torna um objeto "lixo" para o GC?
Por que contagem de referências não coleta ciclos de objetos?
Por que o GC precisa pausar o programa durante a fase de marcação?
⑥ Aplicações no mundo real

Onde você encontra isso

JVM — GC geracional e ZGC

A JVM de produção (OpenJDK) oferece vários coletores: G1 (latência balanceada), ZGC (pausas sub-milissegundo mesmo com heaps de TB), Shenandoah (concurrent compaction). Aplicações como Elasticsearch e Kafka são ajustadas com flags de GC como -Xmx, -XX:+UseZGC para controlar a troca entre throughput e latência.

🐹

Go — GC tricolor concorrente

O GC do Go roda concorrentemente usando um algoritmo tricolor (branco/cinza/preto) com barreiras de escrita. Latências de GC caíram de centenas de ms na versão 1.0 para sub-milissegundo em 1.17+. Go não tem GC geracional — a escolha simplifica a implementação mas limita throughput em cargas com muitos objetos de curta vida.

🌐

V8 — GC do JavaScript

O V8 (Chrome, Node.js) usa Orinoco: um GC geracional, paralelo e concorrente. O young space usa Cheney copying (compacta e é rápido); o old space usa mark-compact. O GC do V8 é um projeto de engenharia próprio — partes dele (como Scavenge) são publicadas como artigos acadêmicos e influenciam outros projetos.

← Anterior: Vazamentos e erros ✓ Concluir trilha: Memória e execução