Parte IV · Gerência de Memória — garbage collection e as alternativas
A gerência automática de memória faz uma pergunta — *quando um pedaço de memória deixa de ser alcançável, e quem o reclama?* — e as respostas partem todo o perfil de performance, latência e ergonomia de uma linguagem. Este capítulo mapeia a área: a família tracing-GC, o renascimento do reference counting, coletores mark-region, e as abordagens que evitam um coletor de runtime por completo. A decisão da Koda vive no
stack-RFC-034; isto é o chão sobre o qual ela se apoia.
1. A família tracing (alcançabilidade a partir de raízes)
Um coletor tracing periodicamente descobre o que é alcançável a partir de um conjunto de raízes (slots de stack, registradores, globais) e reclama o resto.
- Mark-sweep — marca o alcançável, varre o resto. Simples; fragmenta o heap.
- Mark-compact / copying (semi-space) — move os objetos vivos pra junto, eliminando
fragmentação e habilitando alocação bump-pointer (rápida). Custo: move objetos, então precisa de raízes precisas (saber exatamente quais palavras são ponteiros) pra atualizar referências.
- Geracional — a hipótese geracional fraca: a maioria dos objetos morre jovem. Coleta uma
geração jovem pequena com frequência (barato) e promove sobreviventes pra uma geração velha coletada raramente. Um write barrier registra ponteiros velho→jovem. É o cavalo-de-batalha dos runtimes gerenciados e a família que a Koda implementa hoje (young-gen copying + raízes precisas,
#845#730#834). - Concorrente, low-pause — a fronteira moderna: coletar enquanto o programa roda, mantendo
pausas sub-milissegundo mesmo em heaps de TB.
- Azul C4 — o pioneiro da compactação continuamente-concorrente.
- ZGC (OpenJDK) — colored pointers + load barriers, region-based, compactação
concorrente; o ZGC geracional chegou no JDK 21 (2023).
- Shenandoah (Red Hat) — evacuação concorrente via load-reference barriers.
- Go GC — mark-sweep tricolor concorrente, non-moving, write barriers; troca
compactação por simplicidade e pausas curtas.
O custo dessa família: barriers em loads e/ou stores — overhead real de throughput e complexidade profunda de implementação.
Raízes precisas vs conservadoras. Um coletor conservador adivinha quais palavras da stack são ponteiros (não pode mover objetos com segurança; pode vazar). Um coletor preciso sabe exatamente — pré-requisito de qualquer esquema moving/compactante, e o substrato que as abordagens de reference counting abaixo também querem. (A migração da Koda de conservador pra preciso é exatamente esse investimento habilitante.)
2. O renascimento do reference counting
Cada objeto carrega uma contagem de referências; quando chega a zero, é liberado. Por décadas RC foi descartado como "lento e não coleta ciclos". Esse veredito está desatualizado:
- RC deferido / coalescido / enviesado tornou a contagem barata (agrupa e pula updates
redundantes).
- Perceus (Reinking, Xie, de Moura, Leijen — PLDI 2021): o compilador *nsere inc/dec
precisos em tempo de compilação, especializa o
drop, e — a ideia-chave — faz reuse analysis: quando o refcount de um objeto é comprovadamente 1, um update "funcional" é compilado em mutação in-place ("functional but in-place", FBIP). Isso torna o RC competitivo com ou mais rápido que tracing, e determinístico (sem stop-the-world). Usado em Koka e Lean 4 (um theorem prover perf-crítico) e Roc* - LXR (Zuo, Blackburn et al. — PLDI 2022): RC + um backup Immix trace pra ciclos. Bate
coletores tracing de produção (ex.: G1) em latência e throughput simultaneamente — possivelmente o resultado de GC mais forte dos últimos anos.
- Nim ARC/ORC, Swift, Python — linguagens imperativas de produção construídas sobre
RC; ORC adiciona um coletor de ciclos ao ARC.
A pegadinha: RC puro vaza ciclos. O RC moderno se pareia com um coletor de ciclos de backup (LXR, ORC) ou se apoia em ownership/unicidade pra tornar ciclos raros. E a mágica de reuse do Perceus é mais forte em contextos funcionais; em linguagens imperativas/OOP o núcleo RC ainda transfere limpo (Swift/Nim provam) mas o upside FBIP automático é parcial e precisa ser medido.
3. Mark-region e frameworks plugáveis
- Immix (Blackburn & McKinley — PLDI 2008): um coletor mark-region — regiões grossas com
linhas finas, ótima localidade, e evacuação oportunista pra desfragmentar. Base de boa parte da pesquisa moderna de GC (e do backup-trace do LXR).
- MMTk (Memory Management Toolkit, reimplementado em Rust): um framework que deixa um runtime
plugar coletores diferentes por trás de uma interface. Sendo integrado em OpenJDK, V8, Ruby e Julia. O modelo a copiar se uma linguagem quer experimentar coletores sem reescrever o runtime — mas uma dependência pesada de subir.
4. Além do GC — abordagens de tempo-de-compilação e ownership
Algumas linguagens evitam um coletor de runtime resolvendo lifetimes estaticamente:
- Ownership + borrow checking (Rust) — valores de dono-único liberados deterministicamente
ao fim do escopo; o borrow checker prova segurança de aliasing em tempo de compilação. Zero GC, zero pausa, previsível. Custo: fardo real ao programador (a curva do borrow checker).
- Regiões / arenas — aloca numa região, libera a região inteira de uma vez. Manual em
Zig/Odin; inferida em MLKit (region inference Tofte-Talpin) e Cyclone. Velocidade de arena sem bookkeeping por-objeto.
- Tipos lineares / afins — tipos que forçam "use exatamente uma vez / no máximo uma vez",
habilitando o compilador a inserir frees determinísticos. Austral, e a camada RAII-superior do Vale.
- Generational references (Vale) — uma aposta genuinamente diferente: checagens de liveness
baratas via números de geração por-objeto, mais regiões e ownership único, mirando segurança de memória sem GC e sem o borrow checker do Rust. Promissor mas estágio de pesquisa — não production-proven em escala.
5. Como escolher
Não existe coletor universalmente "melhor" — os eixos se contrapõem:
| Eixo | Tracing (concorrente) | RC + reuse (Perceus/LXR) | Ownership / regiões |
|---|---|---|---|
| Pausa / determinismo | pausa baixa, não determinístico | determinístico, sem STW | determinístico (compilação) |
| Throughput vs Rust | overhead (barriers, aloc) | perto-de-Rust se o reuse dispara | =Rust (sem runtime) |
| Ciclos | de graça | precisa de coletor de backup | n/a (sem ciclos por construção) |
| Fardo ao programador | nenhum | nenhum | alto (borrow checker) / médio (regiões) |
| Maturidade | altíssima | alta (LeanKokaSwift) | alta (Rust) / experimental (Vale) |
O ângulo da linguagem greenfield. Uma linguagem novinha com controle total do compilador é o lugar ideal pra gerência de memória assistida por compilação: o coorte moderno de linguagens com-compilador-próprio não escolheu tracing GC — Lean 4 e Koka escolheram Perceus RC + reuse, Rust escolheu ownership, Vale escolheu generational references. Tracing geracional é o modelo de runtimes dinâmicos maduros (Ruby, Go inicial), retrofitado onde o compilador não pode ajudar. Pra uma linguagem nova que também precisa bater uma linguagem de sistema num hot-path, essa distinção é decisiva.
Âncora Koder Stack (D6 — a decisão vive em outro lugar). A escolha da própria Koda neste eixo não é feita neste compêndio. Está registrada no
stack-RFC-034 — Koda memory-management axis(cânone de engenharia): tracing geracional como a ponte pro self-hosted-green, com RC preciso estilo-Perceus + reuse como o norte de longo prazo, gated por um ganho medido contra o gate de performance G2 do self-hosted-first (Koda ≤ Rust × 1.05 num hot-path). Este capítulo é a referência; aquele RFC é a decisão — um fato, um home.