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.
1. 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.
2. 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.

3. 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.
4. 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.
5. 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.
6. Ö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.

7. 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.
Sıkça Sorulan Sorular
Production'da 8 saniyelik GC pause'un en olası nedeni nedir?
Genellikle Parallel GC veya yanlış yapılandırılmış bir collector ile büyük heap kombinasyonudur. Old generation dolduğunda tetiklenen Full GC, 16 GB heap'i single sweep ile tararsa rahatlıkla 5-10 saniye sürebilir. Çözüm G1, ZGC veya Shenandoah gibi concurrent collector'lara geçmektir; ayrıca allocation rate'in düşürülmesi de pause sıklığını azaltır.
G1 ile ZGC arasında nasıl karar verilir?
Heap 4-32 GB arasında ve P99 latency hedefi 100-500 ms ise G1 yeterlidir. Heap 32 GB'ı aşıyorsa veya sub-millisecond pause hedefi varsa ZGC tercih edilir. ZGC pause süresi heap boyutundan bağımsızdır ancak CPU overhead'i G1'e göre yaklaşık yüzde 10-15 daha yüksektir; CPU bütçesi dar sistemlerde bu fark dikkate alınmalıdır.
MaxGCPauseMillis flag'ini çok düşük ayarlamak neden zararlıdır?
Bu parametre bir hedef, garanti değildir. 10 ms gibi ulaşılamaz bir değer verildiğinde JVM region sayısını artırır ve daha sık GC tetikler. Sonuçta hem pause'lar hedeflenenden uzun olur hem de throughput ciddi şekilde düşer. Gerçekçi başlangıç değeri 200 ms, agresif hedef ise 100 ms'dir.
Shenandoah ZGC'den ne ile ayrışır?
Her ikisi de sub-millisecond pause hedefler ancak farklı algoritma kullanır. Shenandoah Brooks pointer ile concurrent compaction yapar ve Red Hat OpenJDK üzerinde geliştirilmiştir. ZGC ise colored pointer ve load barrier tabanlıdır. Düşük gecikme açısından benzer, ancak ZGC daha geniş heap'lerde (terabyte seviyesi) daha iyi ölçeklenir.
GC log'u nasıl açılır ve nasıl analiz edilir?
Java 9+ için JVM'e -Xlog:gc*:file=gc.log:time,uptime,level,tags parametresi eklemek yeterlidir. Java 8'de -XX:+PrintGCDetails -Xloggc:gc.log kullanılır. Log dosyası GCViewer, GCEasy veya jClarity Censum gibi araçlarla analiz edilir; pause süresi dağılımı, allocation rate ve promotion rate metrikleri öne çıkarılır.
Allocation rate'i azaltmak GC tuning'in parçası mıdır?
Evet, çoğu zaman flag ayarlamadan daha etkili olur. Saniyede üretilen nesne miktarı azaldıkça Young GC sıklığı düşer, promotion baskısı azalır ve Old GC daha geç tetiklenir. StringBuilder kullanımı, primitive array tercihleri, object pooling ve boxing'den kaçınma gibi kod düzeyi iyileştirmeler doğrudan GC yükünü hafifletir.
Container ortamında heap boyutu nasıl ayarlanmalıdır?
Sabit -Xmx vermek yerine -XX:MaxRAMPercentage=75.0 gibi yüzde tabanlı bir sınır kullanmak daha güvenlidir. Bu, container memory limiti değiştiğinde JVM'in otomatik uyum sağlamasını mümkün kılar. Ayrıca container'da off-heap kullanımı (Metaspace, direct buffers, thread stacks) için yüzde 20-25 pay bırakmak OOM kill riskini azaltır.
System.gc() çağrısı production kodunda kullanılmalı mıdır?
Hayır. Bu çağrı Full GC tetikler ve uygulamayı uzun süre durdurabilir. Üçüncü parti kütüphanelerin de bu çağrıyı yapmasını engellemek için JVM'e -XX:+DisableExplicitGC flag'i eklenir. Bellek temizliği ihtiyacı varsa doğru yaklaşım collector seçimini ve allocation davranışını gözden geçirmektir, manuel GC tetiklemek değil.


