Parte IV · Gerência de Memória — garbage collection e as alternativas

draft

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.