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.
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.
-Xmxyalnı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
-Xmx= konteyner limiti. Native bellek için yer kalmaz; sonuç OOMKilled ve heap dump'ta hiçbir şey yok.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.
Dev heap ile G1'de duraklama şikâyeti. 48 GB heap'te G1 duraklamaları saniyeye yaklaşabilir; çözüm bayrak değil ZGC'dir.
Üretimde GC logu olmaması. Sorun anında veri yoktur; log maliyeti ihmal edilebilir, her zaman açık olmalı.
Off-heap'i unutmak. Netty, Kafka istemcisi ve sıkıştırma kütüphaneleri heap dışında bellek kullanır;
-XX:MaxDirectMemorySizeve native bellek izleme (-XX:NativeMemoryTracking=summary) gerekir.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.