GC TUNİNG NEDİR? JVM GARBAGE COLLECTOR SEÇİMİ VE PRATİK AYARLAR
Üretimde “ara sıra donuyor” diye tarif edilen performans problemlerinin önemli bir kısmı aslında GC duraklamaları, dengesiz heap boyutları ve yanlış seçilmiş toplayıcı stratejilerinden kaynaklanır. GC tuning, Java uygulamanızın bellek yönetimini “daha fazla RAM ver geçsin” yaklaşımından çıkarıp ölçülebilir, tekrarlanabilir ve sürümleme yapılabilir bir mühendislik pratiğine dönüştürür.
Bu yazıda “GC tuning nedir?” sorusunu pratik bir çerçeveyle ele alacağız: Hangi JVM garbage collector ne zaman seçilir, hangi metriklerle karar verilir, GC logları nasıl okunur ve en sık kullanılan ayarlar hangi senaryolarda anlamlıdır. Amaç; gecikme (latency) hedeflerinizi korurken throughput’u artırmak, bellek tüketimini öngörülebilir kılmak ve sürpriz duraklamaları azaltmaktır.
Okurken şunu akılda tutun: Tuning, çoğu zaman “tek bir sihirli flag” değil; iş yükü, heap yaşam döngüsü, allocation hızı ve SLO birlikte değerlendirilerek yapılan bir dengeleme işidir. Dolayısıyla her bölüm, ölçüm ve doğrulama adımlarına bağlanacaktır.

GC Tuning Nedir ve Neyi Optimize Eder?
GC tuning, JVM’in bellek yönetimi davranışını hedeflediğiniz SLO’lara göre ayarlama sürecidir. Buradaki “optimizasyon”, tek boyutlu değildir; genellikle üç eksen üzerinde düşünülür: duraklama süresi (pause time), toplam iş hacmi (throughput) ve bellek ayak izi (footprint). Bir ekseni iyileştirirken diğerinde maliyet doğurabileceğiniz için, tuning net bir hedefle başlamalıdır.
Örneğin çevrim içi bir ödeme API’si için 99p gecikme kritik olabilir; burada hedef “duraklamaları kısaltmak”tır. Buna karşılık batch işleyen bir ETL uygulamasında asıl hedef “en yüksek throughput” olabilir; kısa süreli duraklamalar kabul edilebilir. Bu yüzden aynı JVM garbage collector seçimi ve aynı parametre seti her projede başarı getirmez.
Temel kavramlar: Allocation, Promotion, Pause
GC’nin performansını belirleyen ana faktörlerden biri allocation hızıdır: uygulama saniyede ne kadar nesne yaratıyor? Kısa ömürlü nesneler young generation’da toplanır; uzun ömürlü nesneler old generation’a “promotion” ile taşınır. Eğer promotion baskısı artarsa, old tarafı daha sık temizlenir ve duraklamalar büyüyebilir.
Hedef metrikler: p95/p99, CPU, heap, GC time
İyi bir tuning çalışması, yalnızca “GC time % kaç” sorusuna bakmaz. p95/p99 gecikme, CPU kullanım profili, heap kullanım eğrisi, allocation rate, old/young oranları ve GC’nin uygulamayı ne sıklıkla durdurduğu birlikte yorumlanır. Ayrıca tuning, tek bir anlık teste değil; benzer yükte tekrarlı koşulara dayanmalıdır.
JVM Garbage Collector Seçimi: Hangi Toplayıcı Ne Zaman?
JDK sürümleri ilerledikçe default seçimler ve toplayıcıların olgunluk seviyeleri değişti. Güncel JVM’lerde temel seçenekler; G1GC, Parallel GC, ZGC ve Shenandoah’tır. Seçim yaparken iki soru belirleyici olur: “Duraklama hedefim ne?” ve “Heap boyutum/iş yüküm ne kadar büyük?”
G1GC: Genel amaçlı, tahmin edilebilir duraklamalar
G1GC, birçok sunucu uygulaması için güvenli bir başlangıçtır. Bölgesel (region) tasarımıyla heap’i parçalara ayırır ve hedef duraklama süresine yaklaşmaya çalışır. Çok sayıda mikroserviste G1GC, minimum ayarla bile stabil bir deneyim sunar; tuning çoğu zaman heap sizing, pause hedefi ve humongous allocation davranışı etrafında şekillenir.
Parallel GC: Maksimum throughput odaklı işler
Parallel GC, duraklamaları kısaltmaktan ziyade toplam iş hacmini artırmaya odaklanır. Batch yükleri, raporlama, gece çalışan job’lar gibi senaryolarda etkili olabilir. Ancak gecikme hassas API’lerde p99 dalgalanmaları büyüyebilir.
ZGC ve Shenandoah: Düşük duraklama hedefli modern seçenekler
ZGC ve Shenandoah, çok düşük duraklamalar için tasarlanmış modern toplayıcılardır. Büyük heap’lerde ve gecikme hassas işlerde avantaj sağlarlar; ancak her ortamda “bedava” değildir: CPU maliyeti, platform uyumluluğu, sürüm özellikleri ve gözlemlenebilirlik pratikleri iyi planlanmalıdır. Bir diğer önemli nokta, bu toplayıcılara geçişte tuning yaklaşımının değişmesidir: bazı klasik parametrelerin etkisi azalır, doğru gözlem metrikleri daha da önem kazanır.
Ölçümle Başlamak: GC Logları, Metrikler ve Gözlemlenebilirlik
GC tuning’in en sık yapılan hatası, ölçüm yapmadan flag eklemektir. Önce “mevcut durum” baz çizgisi çıkarılmalıdır: aynı yük testi, aynı veri seti ve aynı çalışma süresiyle ölçüm alın. Ardından tek tek değişiklik yapıp, etkiyi kıyaslayın. Birden fazla parametreyi aynı anda değiştirmek, hangi değişikliğin fayda sağladığını belirsiz bırakır.
GC loglarını etkinleştirme ve temel alanlar
JDK 9+ ile unified logging kullanarak GC olaylarını tutarlı şekilde yazabilirsiniz. Logları hem olay düzeyinde hem de özet metrikler düzeyinde okumak gerekir: duraklama süreleri, heap occupancy, young/old koleksiyon sıklığı ve concurrent fazların davranışı temel göstergelerdir.
# JDK 11+ örnek GC log ayarı
-Xlog:gc*:file=logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=logs/heapdump.hprof
Log analizi yaparken şunları arayın: p99 gecikmeyle korele uzun duraklamalar, artan “to-space exhausted” benzeri uyarılar, “concurrent cycle”ın sıklaşması ve heap’in yavaşça şişmesi gibi belirtiler. Bu sinyaller, heap sizing veya toplayıcı seçimi tarafında aksiyon gerektirebilir.
JFR ve metriklerle korelasyon
GC logları tek başına yeterli değildir. Uygulama metrikleriyle korelasyon kurmak, “GC mi sebep, sonuç mu?” sorusunu aydınlatır. Örneğin trafikteki ani artış allocation hızını yükseltip GC’yi tetikleyebilir; ya da cache stratejisi değişikliği old generation’ı şişirebilir. Bu noktada Java Flight Recorder (JFR) gibi araçlarla allocation profili ve lock/contention etkilerini görmek değerli olur.
Heap Sizing: Xms/Xmx, Nesne Yaşam Döngüsü ve Metaspace
Heap boyutlandırma, GC tuning’in temel taşıdır. Çok küçük heap; sık GC, yüksek CPU ve jitter üretir. Aşırı büyük heap ise duraklamaları büyütebilir ve container ortamlarında bellek limitleriyle çakışabilir. Ayrıca heap sadece “Java heap” değildir: thread stack’leri, direct buffer’lar, code cache ve metaspace gibi alanlar toplam bellek tüketimini etkiler.
Xms ve Xmx eşitlemek ne zaman mantıklı?
Üretimde sık görülen pratiklerden biri Xms ve Xmx’i eşitlemektir. Bu, heap’in çalışma sırasında büyüyüp küçülmesinden doğan dalgalanmayı azaltabilir ve GC ergonomics’in sürpriz kararlarını sınırlayabilir. Ancak container bellek limitleri olan ortamlarda, heap’i maksimuma sabitlemek OS seviyesinde baskı yaratabilir. Burada karar, toplam bellek bütçesine göre verilmelidir.
Metaspace ve sınıf yükleme davranışı
Framework yoğun uygulamalarda metaspace büyümesi dikkat ister. Özellikle dinamik class generation, yoğun proxy kullanımı veya yanlış classloader yaşam döngüsü, metaspace baskısını artırabilir. Metaspace tuning, doğrudan GC duraklamalarını azaltmasa bile bellek basıncını düşürerek stabilite sağlar.
Aşağıdaki liste, heap sizing için pratik bir kontrol seti sunar:
- Toplam bellek limitinizin (container/VM) net olarak belirlenmesi
- Java heap dışında kalan alanların bütçelenmesi (direct memory, stack, metaspace)
- Allocation rate’in yük testinde ölçülmesi ve değişkenliğinin izlenmesi
- Old generation doluluk trendinin zaman içinde artıp artmadığının kontrolü
- GC sonrası heap’in ne kadar “geri geldiğinin” (live set) izlenmesi

G1GC ile Pratik Ayarlar: Pause Hedefi, Region Mantığı ve Tüzaklar
G1GC, “pause time goal” etrafında çalışan bir yaklaşıma sahiptir. Burada amaç, her duraklamayı kesin bir süreye kilitlemek değil; olasılıksal olarak hedefe yakın kalmaktır. Bu yüzden “sadece MaxGCPauseMillis’i düşürdüm, her şey düzeldi” gibi beklentiler yanıltıcı olabilir. Hedefi aşırı agresif yapmak, CPU maliyetini artırabilir veya heap’i daha sık taramaya zorlayabilir.
-XX:MaxGCPauseMillis: Gerçekçi hedef belirleme
G1GC’de -XX:MaxGCPauseMillis ile hedef duraklama süresi belirlenir. Burada gerçekçi hedef; servisinizin p99 bütçesi, ağ gecikmesi, downstream bağımlılıkları ve iş yükü profiline göre seçilmelidir. Örneğin 50 ms hedefi her sistemde rasyonel değildir; bazen 150–250 ms aralığı daha dengeli sonuç verir.
InitiatingHeapOccupancyPercent ve concurrent döngüler
Old tarafın ne zaman concurrent cycle başlatacağı, gecikme stabilitesi üzerinde etkilidir. InitiatingHeapOccupancyPercent çok düşükse cycle sıklaşır; CPU maliyeti artar. Çok yüksekse old doluluk yükselir ve “geç kalmış” temizleme daha büyük duraklamalara yol açabilir. Burada kritik olan, live set büyüklüğünüzü ve allocation dalgalarını doğru okumaktır.
# G1GC için örnek başlangıç parametreleri (JDK 11+)
-XX:+UseG1GC
-Xms4g -Xmx4g
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=30
-XX:+ParallelRefProcEnabled
-XX:+AlwaysPreTouch
AlwaysPreTouch, özellikle büyük heap’lerde sayfa haritalama gecikmelerini azaltabilir; ancak başlangıç süresini uzatır. ParallelRefProcEnabled, referans işleme maliyetini paralelleştirerek duraklamaları kısaltmaya yardımcı olabilir. Bu parametrelerin etkisi; işletim sistemi, bellek tipi ve iş yüküne göre değişir.
Humongous allocation ve region sınırları
G1GC’de “humongous” olarak sınıflanan büyük nesneler (region boyutuna göre) yönetim maliyeti doğurabilir. Özellikle büyük byte[] veya büyük cache objeleri, old bölgede parçalanma ve beklenmedik cycle davranışı üretebilir. Bu durumda uygulama seviyesinde nesne boyutlarını azaltmak, buffer stratejisini değiştirmek veya farklı toplayıcıya geçmek daha etkili olabilir.

ZGC / Shenandoah: Düşük Gecikme için Yaklaşım Nasıl Değişir?
Düşük gecikme hedefli toplayıcılarda tuning daha çok “heap’i doğru boyutlandırma ve gözlemlenebilirlik” etrafında döner. Çünkü bu toplayıcılar, duraklamaları azaltmak için işi daha fazla eşzamanlı (concurrent) fazlara taşırlar. Sonuç olarak, duraklamalar küçülürken CPU kullanımı artabilir; burada kapasite planlaması kritik hale gelir.
Geçiş stratejisi: A/B test, canary ve geri dönüş planı
ZGC veya Shenandoah’a geçerken doğrudan tüm trafiği taşımak yerine canary yaklaşımı kullanın. Aynı yük altında p99, CPU, RSS bellek tüketimi ve hata oranlarını karşılaştırın. Özellikle container ortamında RSS artışı veya GC cycle davranışı, beklenmedik limit aşımına yol açabilir. Geri dönüş planı (flag seti, rollback) net olmalıdır.
Heap büyüklüğü ve live set davranışı
Bu toplayıcılarda en kritik soru, live set’in ne kadar olduğu ve zamanla nasıl değiştiğidir. Eğer live set çok yüksekse ve heap buna yakınsa, GC daha sık cycle başlatabilir. Heap’i artırmak tek çözüm değildir; cache stratejisi, object pooling gibi yaklaşımlar (yerinde ve ölçümlü şekilde) daha kalıcı fayda sağlayabilir.
Toplayıcı seçimi ve tuning adımlarını pratik bir öğrenme yoluna bağlamak isterseniz, Java Performans & JVM Eğitimi içeriğiyle GC log analizi, JFR ve üretim senaryolarını uçtan uca ele alabilirsiniz.

Yaygın Anti-Pattern’ler: Yanlış Ayarlar ve Yanıltıcı İyileştirmeler
GC tuning’de en tehlikeli noktalar, kısa vadede “iyi görünüp” uzun vadede problemleri büyüten ayarlardır. Örneğin gereksiz yere çok büyük heap vermek, GC’yi seyrekleştirirken duraklamaları büyütebilir; ya da agresif pause hedefi CPU’yu artırıp genel throughput’u düşürebilir. Ayrıca bazı ayarlar sürümler arasında anlam değiştirir; bu yüzden JDK sürüm notlarını takip etmek önemlidir.
Sadece GC time’a bakmak
GC time düşük olsa bile p99 dalgalanabilir. Çünkü kısa ama kritik anlarda gelen duraklamalar kullanıcı deneyimini bozabilir. Bu nedenle, GC metriklerini uygulama gecikmesiyle birlikte okumak gerekir. “Ortalama” değerler tek başına karar verdirmez; dağılım (histogram) daha belirleyicidir.
Heap’i sürekli büyütmek ve bellek bütçesini unutmak
Heap artışı, bir süre sonra OS page cache ve diğer process ihtiyaçlarını sıkıştırabilir. Container’da memory limit aşımlarında OOMKill yaşanabilir. Bu yüzden heap büyütme kararı, toplam bellek ayak izini gözeten bir kapasite planıyla birlikte alınmalıdır.
İş yükünü değiştiren kod değişikliklerini göz ardı etmek
Bir sürümde cache davranışı veya serialization stratejisi değiştiğinde allocation profili tamamen farklılaşabilir. Bu durumda eski tuning parametreleri “doğru” olmaktan çıkabilir. Tuning’i bir defalık iş değil, gözlemlenebilirlik destekli bir yaşam döngüsü olarak ele almak en doğru yaklaşımdır.
Adım Adım Pratik Tuning Akışı: Üretime Uygun Bir Check-list
Aşağıdaki akış, GC tuning’i sistematik hale getirir ve “deneme-yanılma”yı azaltır. Her adımın sonunda ölçüm alın ve önceki baz çizgiyle kıyaslayın. Bir iyileştirmeyi kalıcı saymadan önce farklı saatlerde ve farklı trafik profillerinde doğrulamayı unutmayın.
- SLO’yu netleştirin: p95/p99 hedefleri, kabul edilebilir duraklama aralığı, throughput beklentisi.
- Mevcut durumu ölçün: GC logları + uygulama metrikleri + CPU/RSS bellek trendleri.
- Heap bütçesini belirleyin: container limiti, direct memory, metaspace ve stack payı.
- Toplayıcı seçimini doğrulayın: G1GC varsayılanı mı, düşük gecikme seçeneği mi daha uygun?
- Tek değişkenle ilerleyin: önce heap sizing, sonra pause hedefi, sonra eşik değerleri.
- Regresyon kontrolü yapın: hata oranı, throughput, CPU, bellek tüketimi ve tail latency.
- Parametre setini sürümleyin: JVM flag’leri, test senaryosu ve sonuçları birlikte kaydedin.
Son söz: Tuning’in “en iyi”si değil, “en uygun”u vardır
GC tuning, JVM garbage collector seçimiyle başlar; ancak asıl kazanım ölçüm, yorumlama ve kontrollü değişiklik disiplininden gelir. Uygulamanızın allocation davranışını anlamadan yapılan tuning, çoğu zaman geçici rahatlama sağlar. Doğru hedeflerle, iyi bir gözlemlenebilirlik altyapısıyla ve adım adım ilerleyen bir süreçle ise hem gecikmeyi stabilize edebilir hem de kapasite maliyetini azaltabilirsiniz.
Bu makaledeki yaklaşımı kendi sisteminize uyarlarken, mutlaka benzer yük testleriyle doğrulayın ve değişikliklerinizi küçük adımlarla üretime taşıyın. Böylece GC tuning, korkutucu bir “kara kutu” olmaktan çıkıp sürdürülebilir bir performans pratiğine dönüşür.


