Tuning de GC — metodologia e prática

TL;DR

Tuning de GC não é uma coleção de flags — é mover o ponto de operação no triângulo latência × throughput × footprint, onde melhorar dois eixos quase sempre piora o terceiro. O método é o de qualquer engenharia orientada a medição: baseline com GC log → meta explícita e mensurável → UMA mudança por vez → medir sob carga real → repetir. A maioria dos “tunings” encontrados em produção é cargo cult — flags coladas de blog sem baseline, misturando coletores e versões. E às vezes a resposta certa não é tunar: é trocar de coletor ou consertar o código que aloca demais.

O que é

Tuning de GC é o processo de ajustar o comportamento do coletor para que o perfil de execução da JVM atenda a um requisito medido — de latência, de throughput ou de consumo de memória. A palavra-chave é medido: sem um número de partida e um número de chegada, não existe tuning; existe superstição com sintaxe de flag.

O espaço de decisão é um triângulo com três eixos que puxam em direções opostas:

  • Latência — quanto tempo as pausas STW roubam de cada request. Medida em percentis (p99, p999) e pior caso, nunca em média. É o eixo que aparece no SLA de serviços online.
  • Throughput — fração do tempo total que a aplicação passa fazendo trabalho útil, em vez de coletar lixo. A documentação da Oracle formaliza isso no Parallel via -XX:GCTimeRatio=<N>: a meta é gastar no máximo 1/(1+N) do tempo em GC (default 99 → 1% em GC). É o eixo que importa em batch e ETL.
  • Footprint — quanta memória o processo realmente consome. Coletores low-pause precisam de headroom (folga de heap) para operar concorrentemente; heap maior também significa conta de cloud maior e menos pods por nó.

A regra prática do triângulo: escolha dois. Pausas curtas com throughput alto exigem heap e CPU de sobra (footprint paga). Footprint mínimo com throughput alto aceita pausas longas (latência paga). Pausas curtas com footprint apertado fazem o coletor correr atrás da alocação — e o throughput paga, às vezes com allocation stalls.

Tuning, portanto, é responder três perguntas em ordem: (1) em qual ponto do triângulo o sistema está hoje (baseline)? (2) em qual ponto o SLA exige que ele esteja (meta)? (3) qual é a menor mudança que move o ponto na direção certa — e quanto ela custa nos outros eixos?

Por que importa

É um dos divisores mais nítidos entre senior e cargo cult.

O anti-pattern dominante em produção é a flag colada de blog. Um JAVA_OPTS herdado com oito flags de origens diferentes — metade da era Java 8, uma de coletor removido, duas contraditórias entre si — e ninguém no time sabe dizer qual problema cada uma resolvia, nem se resolvia. Sem baseline, é impossível saber se aquela config ajuda, atrapalha ou é ruído. E como remover flag “que sempre esteve aí” assusta, o lixo se acumula por anos, atravessando upgrades de JDK até o dia em que uma flag morta derruba a JVM no deploy.

A metodologia vale mais que decorar flags. O catálogo de flags muda a cada release; heurísticas internas dos coletores mudam mais ainda (o IHOP do G1, por exemplo, é adaptativo por default — um valor manual que fazia sentido no Java 8 hoje compete com a heurística). O que não muda é o método: medir, declarar meta, mudar uma coisa, medir de novo. Quem domina o método consegue tunar um coletor que ainda não existe; quem decorou flags fica refém do ano em que o blog foi escrito.

Em entrevista, a pergunta “como você faria tuning de GC?” não está pedindo flags — está testando se o candidato responde com processo (“primeiro eu capturo um GC log sob carga real e defino a meta…”) ou com receita (“eu setaria MaxGCPauseMillis=50 e…”). A segunda resposta, sem a primeira, reprova.

Como funciona

O triângulo (latência/throughput/footprint) e o que seu SLA realmente pede

Antes de tocar em qualquer flag, traduza o requisito de negócio para um eixo do triângulo:

Perfil do sistemaEixo dominanteO que o SLA realmente pede
API síncrona com SLA de p99Latênciapausa de GC máxima que cabe no orçamento do p99
Batch/ETL noturnoThroughput% máximo do tempo total gasto em GC (trabalho/hora)
Sidecar, função serverless, pod densoFootprintRSS máximo do processo; pausas e throughput negociáveis

O Parallel GC explicita essa hierarquia na própria documentação: ele atende as metas nesta ordem — primeiro a meta de pausa (-XX:MaxGCPauseMillis, que no Parallel é um hint sem valor default), depois a de throughput (-XX:GCTimeRatio), e só com as duas satisfeitas considera reduzir footprint. Essa ordem é um bom modelo mental para qualquer coletor: declare qual eixo é inegociável e deixe os outros dois flutuarem.

O erro comum é pedir os três: “quero p99 baixo, throughput máximo e o menor heap possível”. Isso não é meta, é desejo. A meta de verdade tem a forma: “pausa de GC p99 < 150ms, aceitando até 5% de throughput a menos e até 20% de heap a mais”.

Metodologia disciplinada

O loop completo, na ordem — pular etapa invalida o resultado:

1. Baseline com GC log. Capture -Xlog:gc* sob carga representativa (produção ou réplica fiel — a leitura do log está em 10 - GC logs — unified logging e leitura). Extraia: distribuição de pausas (p50/p99/pior caso), frequência de coletas, ocorrência de Full GC / evacuation failure, % do tempo em GC, heap após coletas (live-set aproximado). Guarde o arquivo — ele é o “antes” de toda comparação futura.

2. Meta explícita e mensurável. Um número, com unidade e percentil: “pausa p99 < 150ms”, “tempo em GC < 2% do total”, “RSS < 1,5 GB”. Se a baseline já cumpre a meta, não há tuning a fazer — encerrar aqui é o resultado mais comum e mais subestimado do processo.

3. UMA mudança. Uma flag, ou uma troca de coletor, ou um fix de código — nunca duas coisas na mesma rodada. Com duas mudanças simultâneas, o efeito observado não é atribuível a nenhuma delas, e o conhecimento gerado é zero.

4. Medir sob carga real. Mesmo workload, mesma duração, mesmo hardware da baseline. Compare as mesmas métricas do passo 1 — e olhe os três eixos, não só o que você queria melhorar: a pausa caiu, mas o throughput foi junto? O heap cresceu?

5. Repetir — ou parar. Meta atingida → documente a flag, o porquê, o log de antes e o de depois (o futuro time agradece quando for auditar o JAVA_OPTS). Meta não atingida após 2–3 rodadas honestas → o problema provavelmente não se resolve com tuning fino: é troca de coletor, mais recurso, ou código (seções adiante).

Tuning de G1

A recomendação geral da Oracle é deselegante de tão simples: use o G1 com defaults, ajustando no máximo a meta de pausa e o tamanho do heap (-Xmx/-Xms). E, ao migrar de outro coletor, comece removendo todas as flags de GC antigas, não acumulando por cima. Os diais, quando o log justificar:

-XX:MaxGCPauseMillis — o dial principal, e seu custo. Define a meta de pausa (default 200ms). O G1 a cumpre principalmente redimensionando a young generation: meta menor → young menor → pausas mais curtas, porém mais frequentes. O custo é real e documentado: o controle de pausa “incorre em overhead tanto nas threads da aplicação quanto na eficiência de reclamação de espaço”. Meta agressiva demais (10–50ms num heap de vários GB) faz o throughput despencar em troca de pausas que o G1 talvez nem consiga entregar — abaixo de certo ponto, a resposta certa é ZGC, não um número menor aqui. No sentido inverso, se o que falta é throughput, a doc manda relaxar a meta de pausa ou dar mais heap.

-XX:G1HeapRegionSize — quando humongous incomoda. Objetos ≥ metade de uma região viram humongous (caros, ver 06 - Os coletores do HotSpot). Se gc+heap=info mostra muitas regiões humongous comparadas às regiões old, a doc oferece duas saídas: reduzir essas alocações no código (melhor) ou aumentar o tamanho da região com esta flag, para que os mesmos objetos deixem de ser humongous. Efeito colateral documentado: regiões maiores tendem a ter menos referências cruzadas, encolhendo as remembered sets (menos memória auxiliar) — mas também reduzem a granularidade com que o G1 trabalha.

-XX:InitiatingHeapOccupancyPercent (IHOP) — marcação cedo ou tarde demais. O IHOP define a ocupação do heap que dispara o ciclo de marcação concorrente. Por default ele é adaptativo: o G1 calcula o limiar sozinho a partir do comportamento observado. Os dois modos de falha:

  • Marcação tarde demais: a marcação não termina antes do heap encher → evacuation failure → Full GC. Fixes documentados: aumentar o buffer da heurística via -XX:G1ReservePercent, ou desligar a adaptação (-XX:-G1UseAdaptiveIHOP) e fixar -XX:InitiatingHeapOccupancyPercent num valor menor — marcação começa mais cedo.
  • Marcação cedo demais (IHOP manual baixo demais): ciclos concorrentes constantes queimando CPU para reclamar quase nada — throughput pago à toa.

Importante: setar InitiatingHeapOccupancyPercent sem desligar o modo adaptativo tem efeito limitado — em JDKs modernos o valor manual serve de ponto de partida da heurística, não de limiar fixo.

O que não fazer no G1 é explícito na doc: evitar -Xmn, -XX:NewRatio e afins, porque fixar a young “sobrescreve e na prática desabilita o controle de tempo de pausa” — a young dimensionável é o mecanismo central do coletor.

Tuning de ZGC

A filosofia do ZGC é oposta à do G1 em quantidade de diais: ele “foi projetado para ser adaptativo e exigir configuração manual mínima” — redimensiona gerações, escala threads de GC e ajusta tenuring sozinho. Sobram pouquíssimas decisões, todas de dimensionamento:

Heap (-Xmx) — a opção de tuning mais importante, segundo a própria doc. Como o ZGC coleta concorrentemente, o heap precisa acomodar o live-set mais headroom suficiente para servir alocações enquanto o GC roda. Heap justo demais → o coletor não acompanha a taxa de alocação → allocation stalls: threads da aplicação param esperando memória, e a latência que o ZGC prometeu eliminar volta por outra porta. Regra da doc: “quanto mais memória você der ao ZGC, melhor” — equilibrado contra o desperdício.

-XX:SoftMaxHeapSize — limite suave, não teto. Define o tamanho que o ZGC vai se esforçar para não ultrapassar — mas ele pode crescer além, até o -Xmx, se isso for necessário para evitar stall da aplicação. Exemplo da doc: -Xmx5g -XX:SoftMaxHeapSize=4g → heurísticas miram 4 GB, com 1 GB de válvula de escape temporária. É a flag ideal para manter footprint baixo (pods densos, billing por RAM) sem abrir mão da margem de segurança nos picos.

CPU para as fases concorrentes. O trabalho que no Parallel acontecia dentro da pausa, no ZGC acontece em threads concorrentes competindo pelos mesmos cores da aplicação. Dimensionar CPU é parte do tuning: numa máquina saturada, o ZGC degrada — sem cores para marcar e realocar, vêm os stalls. Detalhes operacionais relacionados: por default o ZGC devolve memória não usada ao SO (uncommit, delay default de 300s via -XX:ZUncommitDelay); se latência extrema é o objetivo, a doc recomenda o oposto — -Xms = -Xmx, -XX:+AlwaysPreTouch e desligar uncommit (-XX:-ZUncommit) para eliminar o custo de commit/uncommit em runtime.

Quando TROCAR de coletor em vez de tunar

Tuning move o ponto de operação dentro do envelope de um coletor; se a meta está fora do envelope, nenhuma flag alcança. Sinais de que o perfil não fecha e a resposta é trocar (catálogo e critérios em 06 - Os coletores do HotSpot):

  • Meta de pausa abaixo do que o G1 entrega. SLA pedindo pausas de poucos ms ou menos num heap grande: apertar MaxGCPauseMillis só destrói o throughput. O envelope certo é ZGC (ou Shenandoah, conforme o build).
  • Batch onde a pausa não importa e o G1 está “pagando por nada”. O overhead de controle de pausa do G1 é desperdício se ninguém espera resposta — Parallel entrega mais trabalho por hora.
  • Heap minúsculo em container de 1 vCPU. As estruturas auxiliares do G1 pesam proporcionalmente; Serial tem o menor custo fixo.
  • CPU sem folga para coletor concorrente. ZGC em máquina saturada vive de allocation stall; ou se aumenta CPU, ou se aceita um coletor com pausas (G1) que concentra o trabalho.

Troca de coletor é uma “mudança” no sentido do passo 3 da metodologia: uma por rodada, com baseline antes e medição depois — nos três eixos.

Quando o problema NÃO é GC

Os dois cenários em que tunar o coletor é tratar sintoma:

Alocação excessiva → consertar o código. Se o GC log mostra coletas young a cada poucos segundos com taxa de alocação altíssima, o coletor está fazendo o trabalho dele — quem está errado é a aplicação. Causas típicas: objetos temporários em loop quente, conversões/boxing desnecessários, buffers realocados a cada request, logging que constrói strings gigantes mesmo com o nível desligado. Nenhuma flag reduz alocação; um profiler de alocação (JFR, 13 - JFR e JMC — observabilidade de produção) aponta os sites de alocação dominantes, e o fix é no código. É frequentemente o tuning de maior retorno — e o único que melhora os três eixos ao mesmo tempo.

Memory leak → heap dump, não heap maior. Se o heap ocupado após Full GC cresce monotonicamente ao longo de horas/dias, não há o que tunar: objetos vivos (alcançáveis) não são responsabilidade do coletor. Aumentar -Xmx só agenda o OOM para mais tarde — e de brinde alonga as pausas até lá. O caminho é diagnóstico: heap dump e análise de dominators (12 - Diagnóstico — heap dumps, thread dumps e jcmd).

O teste rápido para distinguir: olhe o heap depois das coletas completas no log. Estável → o live-set é esse mesmo; problema (se houver) é alocação ou dimensionamento. Crescendo sem teto → leak; tuning não se aplica.

Resumo dos diais (referência rápida)

FlagColetorO que fazQuando o log justifica
-Xmx / -Xmstodostamanho do heap — o fator nº 1 de performance de GC segundo a Oraclesempre o primeiro dial; iguais entre si para previsibilidade
-XX:MaxGCPauseMillisG1 (default 200ms); Parallel (hint, sem default)meta de pausa; G1 cumpre encolhendo a youngpausas estourando SLA ou relaxar quando falta throughput
-XX:GCTimeRatioParallel (default 99 → 1% em GC); G1 (default 12 → ~8%)meta de throughput: máx 1/(1+N) do tempo em GCbatch onde % de tempo em GC é a métrica que importa
-XX:G1HeapRegionSizeG1tamanho das regiões; objetos ≥ metade viram humongousgc+heap=info mostrando muitas regiões humongous
-XX:InitiatingHeapOccupancyPercent (+ -XX:-G1UseAdaptiveIHOP)G1ocupação que dispara a marcação concorrente (adaptativo por default)evacuation failure / Full GC por marcação tardia
-XX:SoftMaxHeapSizeZGClimite suave — heurísticas miram nele, mas podem estourar até -Xmx para evitar stallfootprint apertado com necessidade de margem nos picos

A tabela é referência, não receita: nenhuma linha dela se aplica sem o log que a justifique.

Na prática

Cenário completo — mixed pauses estourando o p99

// hipotético: serviço de pedidos (OrderService), Java 21, G1 default,
// -Xmx8g, ~1.200 req/s em pico. SLA: p99 da API < 300ms.
// Sintoma: p99 estourando para 600-900ms em janelas de ~1 minuto, algumas vezes por hora.

Passo 1 — baseline. GC log (-Xlog:gc*:file=gc.log:time,uptime,level,tags) coletado por 24h sob carga real. Leitura:

// hipotético: trechos relevantes da baseline
[…] GC(811) Pause Young (Normal) (G1 Evacuation Pause) 4811M->4302M(8192M) 38.112ms
[…] GC(847) Pause Young (Mixed) (G1 Evacuation Pause) 6913M->4870M(8192M) 412.339ms
[…] GC(848) Pause Young (Mixed) (G1 Evacuation Pause) 6101M->4955M(8192M) 388.516ms
[…] GC(902) To-space exhausted          ← 2 ocorrências em 24h

Diagnóstico da baseline: pausas young normais saudáveis (p99 ~45ms), mas mixed collections de 350–450ms em rajadas — coincidem com as janelas de p99 estourado. As duas ocorrências de To-space exhausted indicam que a marcação concorrente está começando tarde: quando o ciclo termina, a old já está cheia demais, e o G1 precisa de mixed pauses grandes e urgentes para recuperar espaço.

Passo 2 — meta. Pausa de GC p99 < 150ms, aceitando até 3% de throughput a menos e zero To-space exhausted em 24h.

Passo 3 — UMA mudança. Das opções na mesa (relaxar/apertar MaxGCPauseMillis, mexer no IHOP, aumentar heap), a hipótese formada pela baseline aponta para marcação tardia. Mudança escolhida, conforme a doc do G1 para esse sintoma: desligar o IHOP adaptativo e antecipar a marcação.

# hipotético: a única mudança da rodada 1
java -Xmx8g \
  -XX:-G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=35 \
  -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20m \
  -jar order-service.jar

Passo 4 — medir. Mesmas 24h de perfil de carga:

// hipotético: depois da mudança
- Marcação concorrente: 2,1x mais frequente (esperado — começa mais cedo)
- Mixed pauses: máx 142ms, p99 118ms (antes: 350-450ms)
- To-space exhausted: 0 ocorrências
- Throughput: -1,8% (dentro do orçamento de 3%)
- p99 da API: 240ms — dentro do SLA de 300ms

Passo 5 — decisão. Meta atingida na primeira rodada. Para: a flag é documentada no repositório com o porquê, o log de antes e o de depois ficam arquivados como evidência, e nenhuma outra flag é adicionada — a tentação de “já que estamos aqui, otimizar mais” é o começo do cargo cult. Custo aceito e registrado: ciclos de marcação mais frequentes (CPU) e -1,8% de throughput, em troca do p99.

Anti-exemplo — o JAVA_OPTS de cargo cult

O bloco abaixo é o tipo de config que se encontra em Dockerfile herdado — oito flags coladas de blogs de épocas diferentes, sem baseline, sem meta, sem autor:

# hipotético: JAVA_OPTS herdado — NÃO copiar; desmonte linha a linha abaixo
java \
  -XX:+UseG1GC \
  -XX:+UseConcMarkSweepGC \
  -XX:CMSInitiatingOccupancyFraction=70 \
  -Xmn2g \
  -XX:NewRatio=3 \
  -XX:MaxGCPauseMillis=10 \
  -XX:GCTimeRatio=99 \
  -XX:+AggressiveOpts \
  -jar customer-api.jar

O desmonte, flag a flag:

FlagVeredito
-XX:+UseG1GCInócua — G1 já é default desde o Java 9. Só documenta intenção… que as próximas sete contradizem.
-XX:+UseConcMarkSweepGCFatal. CMS foi removido no Java 14; em qualquer JDK moderno, a JVM não sobe (Unrecognized VM option). Também contradiz a flag anterior — dois coletores selecionados.
-XX:CMSInitiatingOccupancyFraction=70Fatal. Flag satélite de um coletor que não existe mais. Mesmo na era CMS, sem UseCMSInitiatingOccupancyOnly ela era só um chute inicial.
-Xmn2gSabotagem do G1. A doc manda explicitamente evitar: fixar a young “praticamente desabilita o controle de tempo de pausa”. A meta de pausa da linha de baixo vira decorativa.
-XX:NewRatio=3Mesmo pecado da anterior — e conflita com ela (duas formas de fixar a young com valores potencialmente incompatíveis).
-XX:MaxGCPauseMillis=10Meta irreal para um heap de serviço comum no G1. O coletor encolhe a young ao mínimo tentando cumprir, minors disparam, throughput despenca — e 10ms não chega. Se 10ms é requisito real, a resposta é ZGC, não este número.
-XX:GCTimeRatio=99Default do Parallel (1% em GC) colado num contexto G1, onde o default é 12 (~8%). Junto com a meta de pausa de 10ms, declara duas metas mutuamente impossíveis — pausas mínimas E tempo de GC mínimo.
-XX:+AggressiveOptsFlag morta — deprecada no JDK 11, obsoleta no 12 (emite warning), expirada no 13 (derruba a JVM com Unrecognized VM option); em qualquer JDK atual derruba a JVM. Nunca teve semântica estável: era um pacote de opções experimentais que mudava por release.

Resultado líquido do bloco: a JVM nem inicia num JDK ≥ 14. E se as flags fatais fossem removidas, o resto ainda seria um G1 sabotado perseguindo metas contraditórias. O fix não é ajustar — é zerar: voltar para -Xmx + GC log, capturar baseline e só readicionar o que um log justificar.

Armadilhas

(1) Tunar sem baseline e sem meta

O problema: mudar flags “para ver se melhora” sem um GC log de antes nem um número-alvo. Qualquer resultado é ininterpretável: se o p99 caiu, foi a flag ou a carga que mudou? Se nada mudou, a flag é inócua ou o problema está em outro lugar? Sem baseline, até uma melhora real é indistinguível de ruído — e a flag fica lá para sempre, protegida pelo medo de remover.

# hipotético: "tuning" sem evidência — o resultado não ensina nada
java -XX:MaxGCPauseMillis=100 -Xmx6g -jar customer-api.jar
# Melhorou? Comparado com o quê? Medido onde?

Fix: GC log antes de qualquer mudança (10 - GC logs — unified logging e leitura) e meta numérica declarada (percentil + unidade). Se a baseline já cumpre a meta, o tuning acabou antes de começar.


(2) MaxGCPauseMillis agressivo demais

O problema: tratar a meta de pausa do G1 como botão de “deixar rápido” e cravar 20–50ms num heap de vários GB. O G1 só tem uma alavanca principal para cumprir: encolher a young. Young minúscula → minors constantes → o overhead fixo de cada pausa se paga centenas de vezes por minuto → throughput despenca. A doc é explícita: o controle de pausa custa overhead nas threads da aplicação e na eficiência de reclamação — e meta irreal maximiza esse custo sem entregar a pausa pedida.

# hipotético: meta irreal — throughput cai, e os 20ms não acontecem
java -XX:MaxGCPauseMillis=20 -Xmx8g -jar order-service.jar

Fix: se o log mostra o G1 falhando a meta consistentemente, relaxe a meta (ou aumente o heap), conforme a recomendação da Oracle para throughput. Se a pausa exigida pelo SLA está genuinamente abaixo do envelope do G1, a resposta é trocar para ZGC — não torturar o dial.


(3) “Resolver” leak com -Xmx maior

O problema: OOM ou heap crescendo → reflexo de dobrar o -Xmx. Se a causa é leak (objetos alcançáveis acumulando), heap maior só adia o OOM — agora ele acontece de madrugada do dia seguinte em vez de à tarde — e, de brinde, as coletas completas que varrem esse heap inflado ficam proporcionalmente mais longas. O sistema ganha um modo de falha pior: mais raro, mais lento e mais difícil de reproduzir.

# hipotético: o leak agradece o prazo estendido
java -Xmx16g -jar order-service.jar   # antes: -Xmx8g, OOM a cada ~20h
# resultado: OOM a cada ~40h, com Full GCs de pausa dobrada no caminho

Fix: o teste do live-set — heap ocupado após coletas completas crescendo sem teto = leak. Aí o caminho é heap dump e análise de dominators (12 - Diagnóstico — heap dumps, thread dumps e jcmd), nunca flag. -Xmx maior é legítimo apenas quando o live-set é estável e genuinamente maior que o heap atual comporta.


(4) Tunar várias flags de uma vez

O problema: rodada de tuning que muda quatro flags juntas “para ganhar tempo”. O resultado — bom ou ruim — não é atribuível a nenhuma delas: se melhorou, qual segurar? Se piorou, qual reverter? E interações entre flags são reais (uma young fixada anula a meta de pausa, um IHOP manual compete com a heurística adaptativa). O conhecimento gerado pela rodada é zero, e o JAVA_OPTS ganhou quatro inquilinos permanentes.

# hipotético: 4 mudanças, 0 conclusões possíveis
java -XX:MaxGCPauseMillis=100 -XX:G1HeapRegionSize=16m \
     -XX:InitiatingHeapOccupancyPercent=40 -XX:ConcGCThreads=4 \
     -jar order-service.jar

Fix: uma mudança por rodada, sempre — mesmo que cada rodada custe um ciclo de medição. É mais lento por rodada e muito mais rápido até a causa: cada medição produz uma conclusão definitiva sobre uma variável. Disciplina de experimento, não de pressa.

Em entrevista

Frase pronta (inglês)

“I treat GC tuning as a measurement-driven loop, not a bag of flags. First I capture a GC log under realistic load and establish a baseline — pause percentiles, collection frequency, any Full GCs — and I state an explicit, measurable goal, like ‘GC pause p99 under 150 milliseconds’. Then I change exactly one thing per iteration and re-measure under the same load, watching all three axes — latency, throughput, and footprint — because improving one almost always taxes another.”

“The flags themselves are secondary to the method, but the dials I reach for are well documented: on G1, MaxGCPauseMillis is the main one — and the trade-off is explicit, since G1 meets a tighter pause goal mostly by shrinking the young generation, which means more frequent collections and lower throughput; if marking starts too late and I see evacuation failures, I look at the adaptive IHOP. On ZGC there’s much less to tune — it’s mostly about heap sizing with enough headroom for the concurrent cycles, plus SoftMaxHeapSize when I want to keep the footprint down without losing the safety margin.”

“The caveat I always add: a lot of what looks like a GC problem isn’t one. If allocation rate is the issue, the fix is in the code, not in the collector; if the live-set grows without bound, that’s a leak and the answer is a heap dump, not a bigger Xmx. And sometimes the honest conclusion of the baseline is that the pause goal is simply outside the collector’s envelope — then I switch collectors instead of torturing flags.”

Vocabulário

Termo PTTermo EN
linha de basebaseline
meta de tempo de pausapause-time goal
meta de throughputthroughput goal
folga de heapheap headroom
limite suave de heapsoft maximum heap size
conjunto vivo (objetos alcançáveis)live-set
falha de evacuaçãoevacuation failure
limiar de ocupação para marcaçãoinitiating heap occupancy (IHOP)
parada por falta de alocaçãoallocation stall
pegada de memória(memory) footprint
tuning por imitação sem evidênciacargo cult tuning
uma mudança por rodadaone change per iteration

Veja também

Referências