in

JVM Garbage Collector Seçimi: G1, ZGC ve Shenandoah

Çoğu Spring Boot uygulaması için varsayılan G1 doğru seçimdir; ama p99 gecikme hedefi, 32 GB üstü heap ya da 512 MB'lık konteynerler kararı değiştirir. JVM bellek bölgelerini, G1, ZGC, Shenandoah, Parallel ve Serial GC'nin hangi yükte kazandığını, konteyner bellek ayarlarını ve GC logu ile ölçmeyi anlatıyoruz.

G1, ZGC ve Shenandoah çöp toplayıcılarının duraklama süresi ve verim eksenlerinde karşılaştırması

JVM'in çöp toplayıcısı (GC), kurumsal uygulamalarda en çok "ayarlanan" ve en az anlaşılan bileşendir. İki uç görürüz: forumdan kopyalanmış on beş bayraklık bir JAVA_OPTS ve GC'nin varlığını konteyner OOMKilled olana kadar hatırlamayan ekipler. Gerçek şu ki çoğu Spring Boot uygulaması için varsayılan G1 doğru seçimdir; ama p99 gecikme hedefi olan bir API, 32 GB üstü heap ya da 512 MB'lık bir konteyner kararı değiştirir.

Bu yazıda JVM belleğinin heap'ten ibaret olmadığını, G1, ZGC, Shenandoah, Parallel ve Serial GC'nin hangi yükte kazandığını, konteyner bellek ayarlarını, GC logu ve JFR ile ölçmeyi ve sahada gördüğümüz yaygın hataları anlatıyoruz.

JVM belleği heap'ten ibaret değildir

Konteyner 2 GB limitli, -Xmx2g verilmiş ve uygulama OOMKilled oluyor: en sık gördüğümüz şikâyet budur. JVM'in toplam bellek kullanımı (RSS) heap'in üstündedir:

  • Heap: Nesneler; young (eden + survivor) ve old bölgeleri. -Xmx yalnızca bunu sınırlar.

  • Metaspace: Sınıf meta verisi; Spring uygulamalarında 100–200 MB normaldir.

  • Code cache: JIT derlenmiş kod; 50–240 MB.

  • Thread yığınları: Platform thread başına ~1 MB; 200 Tomcat thread'i 200 MB demektir.

  • Direct/native bellek: Netty tamponları, dosya eşlemeleri, JDBC sürücüleri, sıkıştırma kütüphaneleri.

  • GC'nin kendi yapıları: G1 için heap'in %5–10'u, ZGC için değişken.

Pratik kural: konteyner limitinin en fazla %70–75'i heap olmalıdır. Sabit -Xmx yerine yüzde vermek konteyner boyutu değiştiğinde ayarı otomatik ölçekler:

# Konteyner limiti 2 GB ise heap ≈ 1,5 GB; kalanı metaspace, thread ve native bellek için
JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:InitialRAMPercentage=50 \
  -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps \
  -Xlog:gc*,gc+heap=info:file=/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20m"

JVM 10'dan beri konteyner farkındadır: cgroup limitini "toplam bellek" olarak görür ve CPU kotasına göre GC thread sayısını belirler. İki CPU'dan az ya da yaklaşık 1,8 GB'tan az bellek gören JVM, kendini "sunucu sınıfı makine" saymaz ve Serial GC seçer — küçük konteynerlerde G1 kullandığınızı sanıp Serial ile çalışıyor olabilirsiniz; -Xlog:gc ilk satırda hangi GC'nin seçildiğini yazar.

Toplayıcılar: ne için tasarlandılar?

GC

Hedef

Tipik duraklama

Verim maliyeti

Bellek payı

Uygun senaryo

Serial

Basitlik, küçük heap

Heap boyutuyla orantılı; küçük heap'te ms

Tek thread; küçük heap'te ihmal edilebilir

En düşük

<1 GB konteynerler, CLI araçları, sidecar'lar

Parallel

Maksimum verim

Uzun (yüzlerce ms – sn), ama seyrek

En düşük toplam CPU maliyeti

Düşük

Toplu işler, ETL, gece raporları; gecikme önemsiz

G1 (varsayılan)

Verim ile duraklama dengesi

Hedef 200 ms (ayarlanabilir); genelde 10–100 ms

Orta

Orta (%5–10)

Çoğu web/API uygulaması, 1–32 GB heap

ZGC (generational)

Milisaniye altı duraklama, büyük heap

<1 ms, heap boyutundan bağımsız

Orta–yüksek (eşzamanlı çalışma CPU yer)

Yüksek (headroom gerekir)

Gecikmeye duyarlı servisler, 16 GB+ heap, p99 hedefi

Shenandoah

Düşük duraklama

ms mertebesi, heap'ten bağımsız

Orta–yüksek

Orta–yüksek

ZGC'ye benzer; OpenJDK dağıtımlarında (Oracle JDK'da yok)

Modern GC'lerin farkını tek cümleyle: Parallel "durdur ve hızlıca temizle", G1 "bölge bölge, hedef süreye sığdır", ZGC ve Shenandoah "uygulamayı neredeyse hiç durdurmadan eşzamanlı temizle" der. Eşzamanlılık bedavaya gelmez; toplayıcı uygulamayla aynı anda CPU harcar ve uygulamanın tahsis hızına yetişebilmek için boş heap (headroom) ister.

Seçim rehberi

Durum

Öneri

Neden

Tipik Spring Boot API, 2–8 GB heap, p99 < 300 ms hedefi

G1, varsayılan ayarlarla

Dengeli; ayar gerektirmeden hedefi tutar

p99 < 50 ms hedefi, gecikme dalgalanması kabul edilemez

Generational ZGC

Duraklama heap boyutundan bağımsız; CPU ve bellek payı bütçelenmeli

32 GB+ heap (önbellek, arama, analitik)

ZGC

G1'in duraklamaları heap'le büyür; ZGC'ninki büyümez

Gece toplu işi, ETL, rapor üretimi

Parallel

Toplam süre önemli, duraklama önemsiz; en düşük CPU maliyeti

512 MB – 1 GB konteyner, sidecar, küçük servis

Serial (JVM zaten seçebilir)

GC yapılarının bellek maliyeti en düşük

Oracle JDK dışı dağıtım, düşük duraklama, orta heap

Shenandoah ya da ZGC

İkisi de düşük duraklama; ölçümle karar

# G1: duraklama hedefi ve string tekilleştirme (aynı metnin çok kopyası varsa)
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+UseStringDeduplication

# Generational ZGC (JDK 21: bayrakla; JDK 23+: varsayılan ZGC modu)
-XX:+UseZGC -XX:+ZGenerational -XX:SoftMaxHeapSize=6g

# Parallel: toplu işler
-XX:+UseParallelGC -XX:ParallelGCThreads=4

Ölçmeden karar vermeyin

GC seçimi ve ayarı bir ölçüm sorunudur; hangi bayrağın işe yaradığı ancak üretim benzeri yük altında görünür. Üç kaynak yeterlidir:

  • GC logu (-Xlog:gc*): duraklama süreleri, sıklığı, heap'in toplama öncesi/sonrası boyutu. GCEasy, GCViewer gibi araçlarla dakikalar içinde okunur. Bakılacaklar: sık old-gen (mixed/full) toplama, toplama sonrası heap'in küçülmemesi (sızıntı), G1'de humongous tahsisler.

  • JFR (jcmd <pid> JFR.start duration=120s filename=kayit.jfr): tahsis hızını ve hangi kodun en çok nesne ürettiğini gösterir. Çoğu GC "sorunu" aslında bir tahsis sorunudur: N+1 sorgular, DTO yerine entity yüklemek, döngüde string birleştirme.

  • Micrometer metrikleri (jvm.gc.pause, jvm.memory.used, jvm.gc.memory.allocated): Grafana'da uzun vadeli eğilim ve alarm. Sürüm sonrası tahsis hızı iki katına çıktıysa GC ayarı değil kod değişmiştir.

Yaygın hatalar

  1. -Xmx = konteyner limiti. Native bellek için yer kalmaz; sonuç OOMKilled ve heap dump'ta hiçbir şey yok.

  2. Küçük konteynerde ZGC. Eşzamanlı toplayıcının headroom ve CPU ihtiyacı 1 GB'lık konteynerde G1'den kötü sonuç verir.

  3. Dev heap ile G1'de duraklama şikâyeti. 48 GB heap'te G1 duraklamaları saniyeye yaklaşabilir; çözüm bayrak değil ZGC'dir.

  4. Üretimde GC logu olmaması. Sorun anında veri yoktur; log maliyeti ihmal edilebilir, her zaman açık olmalı.

  5. Off-heap'i unutmak. Netty, Kafka istemcisi ve sıkıştırma kütüphaneleri heap dışında bellek kullanır; -XX:MaxDirectMemorySize ve native bellek izleme (-XX:NativeMemoryTracking=summary) gerekir.

  6. Kodu düzeltmek yerine GC'yi ayarlamak. Saniyede 2 GB tahsis eden bir uygulamayı hiçbir GC kurtarmaz; önce tahsisi düşürün.

Java 21 ve 25 ile değişenler

  • Generational ZGC (JDK 21'de seçmeli, 23'ten itibaren varsayılan ZGC modu): genç nesneleri ayrı toplayarak ZGC'nin CPU ve bellek maliyetini belirgin düşürdü; ZGC'yi "yalnızca dev heap" seçeneği olmaktan çıkardı.

  • Sanal thread'ler: Thread yığınları artık heap'te yaşar; yüz binlerce sanal thread'in bellek etkisi GC ile ölçülür, thread sayısıyla değil.

  • Kompakt nesne başlıkları (JDK 25, -XX:+UseCompactObjectHeaders): nesne başlığı 12 bayttan 8'e iner; küçük nesne ağırlıklı uygulamalarda heap %10–20 küçülür ve önbellek kullanımı iyileşir. Ücretsiz kazanç; test edip açın.

Sık sorulan sorular

Varsayılan ayarlarla üretime çıkabilir miyiz?

Evet, çoğu API uygulaması için: G1, MaxRAMPercentage=75, heap dump ve GC logu açık. Bunun ötesindeki her bayrak, bir ölçümün gösterdiği somut soruna karşılık gelmelidir.

Full GC neden oluyor, nasıl önlerim?

G1'de full GC, eşzamanlı toplama tahsis hızına yetişemediğinde ya da heap gerçekten dolduğunda olur. Önce sızıntıyı eleyin (toplama sonrası heap küçülüyor mu?), sonra tahsis hızını düşürün, en son heap'i büyütün ya da ZGC'ye geçin.

Heap büyütmek her zaman iyi midir?

Hayır. Gereksiz büyük heap G1'de duraklamayı uzatır, konteyner maliyetini artırır ve sızıntıyı gizler. Heap'i, toplama sonrası canlı veri kümesinin 2–3 katı olacak şekilde boyutlandırın.

Sonuç

GC seçimi bir yük profili sorusudur: dengeli API'lerde G1, gecikmeye duyarlı ya da dev heap'li servislerde generational ZGC, toplu işlerde Parallel, küçük konteynerlerde Serial. Konteyner limitinin %75'i heap, GC logu ve heap dump her zaman açık, her ayar bir ölçüme dayalı — bu üç kural GC kaynaklı üretim sorunlarının büyük kısmını önler.

Üretim uygulamanızın bellek ve GC davranışını ölçüp doğru yapılandırmayı kurmak için bakım ve destek hizmetimize göz atabilirsiniz.