JAVA GC TUNING NEDİR?
Production'da kullanıcı isteği bir anda 8 saniye boyunca yanıtsız kaldı. Loglarda hata yok, CPU normal, ağ temiz — ama timeout yağıyor. Sonra GC log'una bakıldığında görüldü ki tek bir Full GC tüm uygulama thread'lerini 8.2 saniye boyunca durdurmuş. İşte GC tuning bu noktada devreye girer: çöp toplayıcının davranışını uygulamanın yük profiline göre ayarlamak, "stop-the-world" pause'larını milisaniyelere indirmek ve hangi collector'ı (G1, ZGC, Shenandoah) hangi senaryoda seçeceğini bilmek demektir.
GC Tuning Nedir, Ne Zaman Gerekir?
GC tuning, JVM'in heap'i nasıl bölümlendirdiğini, ne sıklıkla temizlik yaptığını ve bu temizlik sırasında uygulamayı ne kadar süre durdurduğunu kontrol etme işidir. Varsayılan ayarlar küçük servisler için yeterlidir; ancak heap 4 GB'ı geçtiğinde, allocation rate saniyede yüzlerce MB'a çıktığında veya P99 latency hedefi 100 ms altına indiğinde tuning kaçınılmaz hale gelir.
Tuning'e başlamadan önce şu üç soruya cevap aranmalıdır:
- Throughput mu, latency mi öncelik? Batch işlemde toplam süre, web servisinde tek istek pause süresi önemlidir.
- Heap ne kadar büyük olacak? 8 GB altı için G1, 32 GB üzeri için ZGC veya Shenandoah daha mantıklıdır.
- Allocation rate nedir? Saniyede ne kadar nesne üretiliyor — bu, Young GC sıklığını doğrudan belirler.
8 Saniyelik Pause Nereden Geliyor?
Senaryo şudur: 16 GB heap, Parallel GC (Java 8 default), allocation rate yüksek, eski nesneler tenured generation'a hızlıca terfi ediyor. Old gen dolduğunda Parallel GC tek seçeneğine başvurur — Full GC. Bu, single-threaded olmasa bile tüm uygulamayı durdurarak heap'i baştan sona tarar. 16 GB için bu tarama rahatlıkla 5-10 saniye sürebilir.
Bu pause'u görmek için JVM'i şu flag'lerle çalıştırmak gerekir -Xlog:gc*:file=gc.log:time,uptime,level,tags. Log'da Pause Full (Allocation Failure) satırı 8000 ms süre gösteriyorsa kök neden bellidir collector seçimi yanlıştır.

G1, ZGC ve Shenandoah Arasında Seçim Kriterleri
Modern JVM'de üç ciddi seçenek bulunur. Her birinin tasarım önceliği farklıdır:
- G1GC (Garbage First): Java 9+ default. Heap'i region'lara böler, pause hedefi
-XX:MaxGCPauseMillisile yönetilir. 4-32 GB heap için en dengeli seçimdir. Tipik pause 50-200 ms aralığındadır. - ZGC: Java 15'te production-ready oldu. Pause süresi heap boyutundan bağımsız — 16 GB'da da, 16 TB'da da 1 ms altı kalır. Concurrent marking ve relocation kullanır. CPU overhead'i G1'e göre %10-15 daha yüksek olabilir.
- Shenandoah: Red Hat OpenJDK'da geliştirildi. ZGC ile benzer hedef (sub-millisecond pause) ancak farklı algoritma — Brooks pointer ile concurrent compaction yapar. Düşük gecikmeli işlem sistemleri için tercih edilir.
Seçim için pratik bir kural: P99 latency hedefi 200 ms'in altında ise ve heap 16 GB'ı aşıyorsa, G1 çoğunlukla yetersizdir; ZGC veya Shenandoah'a geçmek gerekir. Heap küçükse ve throughput öncelikli ise Parallel GC hâlâ geçerli bir seçenektir. Collector davranışları ve flag etkileşimleri hakkında derin bir kaynak arayanlar HotSpot JVM'in resmi tuning kılavuzuna başvurabilir.
Heap Boyutlandırma ve Temel Flag'ler
Tuning'in ilk adımı doğru heap boyutudur. -Xms ve -Xmx her zaman eşit tutulmalıdır; aksi halde JVM çalışma sırasında heap büyütme/küçültme maliyetine girer. Container ortamlarında -XX:MaxRAMPercentage=75.0 ile dinamik sınır kullanmak daha güvenlidir.
G1 için kritik flag'ler şunlardır:
-XX:+UseG1GC— G1'i aktive eder (Java 9+ default).-XX:MaxGCPauseMillis=200— hedef pause süresi; JVM region sayısını buna göre ayarlar.-XX:G1HeapRegionSize=16m— region boyutu; büyük nesneler için artırılır.-XX:InitiatingHeapOccupancyPercent=45— concurrent marking ne zaman başlasın.
ZGC için ise sadece -XX:+UseZGC -XX:+ZGenerational ile başlamak yeterlidir; ZGC kendi kendini büyük ölçüde ayarlar. Tuning yapılacak nokta heap boyutu ve -XX:ConcGCThreads sayısıdır.
Allocation Rate ve Object Pretenuring
GC pause süresi sadece collector'a bağlı değildir; uygulamanın ne kadar çöp ürettiği de belirleyicidir. Saniyede 500 MB allocation yapan bir servis, saniyede 50 MB yapan servise göre 10 kat daha sık GC tetikler. Bu durumda kod tarafında:
- Gereksiz string concatenation yerine
StringBuilderkullanmak, - Loop içinde her iterasyonda yeni nesne yaratmamak,
- Büyük byte array'leri pool'lamak (Netty'nin
ByteBufpool'u gibi), - Boxing/unboxing tuzaklarından kaçınmak (özellikle
Longmap key'leri)
gibi pratikler GC yükünü doğrudan azaltır. JVM'in iç çalışma prensiplerini ve allocation davranışını daha derin anlamak için Java performans ve JVM eğitimi içeriğinden yararlanabilirsiniz.
Ölçme: Tuning'siz Tuning Olmaz
"Şu flag'i ekledim, hızlandı" yaklaşımı tehlikelidir. Her değişikliğin etkisini ölçmek gerekir. Temel ölçüm araçları:
- GC logs:
-Xlog:gc*ile detaylı kayıt;GCViewerveyaGCEasyile analiz. - JFR (Java Flight Recorder): Düşük overhead'li profiling; pause dağılımı ve allocation hotspot'ları gösterir.
- Async-profiler: Allocation profili çıkarmak için en pratik araç.
- Production monitoring: Micrometer + Prometheus üzerinden
jvm_gc_pause_secondsmetriği.

Tipik Hatalar ve Anti-Pattern'ler
Tuning sürecinde sık rastlanan yanlışlar:
- Heap'i sınırsız büyütmek: Daha büyük heap daha uzun mark fazı demektir; G1'de 64 GB'tan sonra pause artmaya başlar.
- MaxGCPauseMillis'i 10 ms yapmak: JVM bu hedefe ulaşamaz, sadece daha sık GC tetikler ve throughput düşer.
- System.gc() çağırmak: Full GC tetikler; production kodunda yeri yoktur,
-XX:+DisableExplicitGCile kapatılır. - Tuning'i benchmark olmadan yapmak: Stage ortamında load test olmadan flag değiştirmek, üretimde sürprize yol açar.
JVM performansı, collector seçimi ve heap profilleme konusunda derinleşmek isteyenler JVM performans eğitimi kapsamında daha ileri konuları inceleyebilirsiniz.
GC tuning bir sefer yapılıp bitirilen iş değildir. Trafik profili değiştikçe, yeni endpoint'ler eklendikçe, heap kullanımı evrildikçe ayarların yeniden değerlendirilmesi gerekir. Önemli olan, her değişikliğin ölçüme dayalı yapılması ve "varsayılan ayarlar yeter" varsayımının ne zaman geçersizleştiğini fark edebilmektir.



