JVM Bellek Ayarları: Heap Size ve Garbage Collection Optimizasyonu
Java uygulamalarının production ortamında beklenmedik şekilde çökmesi, “OutOfMemoryError” fırtınası, sürekli Full GC döngüsüne giren ve kullanıcılara “uygulama yanıt vermiyor” hissettiren sistemler… Bunların hepsiyle uğraştıysanız, JVM bellek yönetimini doğru yapılandırmamış olmanın acısını biliyorsunuzdur. Bu yazıda JVM heap boyutunu nasıl ayarlayacağınızı, hangi garbage collector’ın hangi senaryoda daha iyi çalıştığını ve production ortamında gerçekten işe yarayan optimizasyon tekniklerini ele alacağız.
JVM Bellek Mimarisini Anlamak
JVM belleği sadece heap’ten ibaret değildir. Bunu doğru anlamadan yapılan ayarlamalar genellikle sorunu çözmek yerine başka yere taşır.
JVM bellek alanları şunlardır:
- Heap: Nesnelerin yaşadığı alan. Young Generation ve Old Generation olarak ikiye ayrılır
- Metaspace: Sınıf metadata bilgilerinin tutulduğu alan (Java 8 öncesinde PermGen’di)
- Stack: Her thread için ayrılan, method çağrılarını ve lokal değişkenleri tutan alan
- Code Cache: JIT compiler tarafından derlenen native kodun saklandığı alan
- Direct Buffer: NIO işlemleri için heap dışında ayrılan bellek
Çoğu sysadmin sadece heap ile ilgilenip diğer alanları göz ardı eder. Sonra “heap yeterince büyük ama uygulama hala crash oluyor” diye şaşırır. Docker container içinde çalışan bir Java uygulaması düşünün: Heap’e 2GB ayırdınız, Metaspace 256MB yedi, thread stack’leri 500MB tüketti, container limiti 3GB. Sonuç: OOMKilled.
Temel Heap Size Parametreleri
JVM’in en kritik parametrelerinden başlayalım. Bunları doğru ayarlamak performansın temelini oluşturur.
# Temel heap size ayarı
java -Xms512m -Xmx2g -jar uygulama.jar
# Xms: Başlangıç heap boyutu
# Xmx: Maksimum heap boyutu
# Young generation boyutunu ayarlamak
java -Xms1g -Xmx4g -Xmn1g -jar uygulama.jar
# Metaspace limitini ayarlamak (Java 8+)
java -Xms1g -Xmx4g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -jar uygulama.jar
Önemli nokta: -Xms ve -Xmx değerlerini production’da eşit tutmak genellikle iyi bir pratiktir. JVM heap’i büyütürken OS’dan bellek talep etmek zaman aldığından, baştan maksimum belleği ayırmak latency spike’larını önler.
# Production için önerilen temel ayar
java
-Xms4g
-Xmx4g
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+UseG1GC
-jar uygulama.jar
Sistem belleğinin yüzde kaçını heap’e vermeli sorusunun cevabı şöyledir: Genellikle toplam RAM’in yüzde 70-75’i güvenli bir başlangıç noktasıdır. 16GB RAM’li bir sunucuda 12GB heap mantıklıdır. Ama bu her zaman doğru değil; OS’un dosya sistemi cache’i için de yer bırakmanız gerekir, özellikle disk I/O yoğun uygulamalarda.
Garbage Collector Seçimi
Java’nın sunduğu GC seçenekleri arasında doğru birini seçmek, uygulamanızın karakterine bağlıdır. Her GC’nin farklı trade-off’ları vardır.
Serial GC
Tek thread’li, küçük uygulamalar için uygundur. CLI araçları veya batch işlemlerde kullanılabilir.
java -XX:+UseSerialGC -Xmx512m -jar kucuk-uygulama.jar
Parallel GC (Throughput Collector)
Java 8’in varsayılan GC’si. Yüksek throughput gerektiren batch işlemler için idealdir. Pause sürelerine toleranslıysanız tercih edin.
java
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
-XX:MaxGCPauseMillis=200
-Xms8g
-Xmx8g
-jar batch-islem.jar
G1GC (Garbage First)
Java 9 sonrasının varsayılan GC’si. Büyük heap’lerde tutarlı pause süreleri sağlar. 4GB ve üzeri heap için önerilir.
java
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=45
-Xms8g
-Xmx8g
-jar web-uygulama.jar
G1GC parametreleri:
- MaxGCPauseMillis: Hedef maksimum pause süresi (milisaniye)
- G1HeapRegionSize: Heap region boyutu (1MB-32MB arası, 2’nin kuvveti olmalı)
- InitiatingHeapOccupancyPercent: Concurrent GC cycle’ı başlatmak için heap doluluk eşiği
ZGC
Düşük latency kritik sistemler için. Pause süreleri milisaniyenin altında kalır. Java 15’ten itibaren production-ready.
java
-XX:+UseZGC
-Xms16g
-Xmx16g
-XX:ConcGCThreads=4
-jar dusuk-latency-uygulama.jar
Shenandoah GC
Red Hat tarafından geliştirilen, ZGC’ye benzer düşük latency GC’si. OpenJDK’da mevcuttur.
java
-XX:+UseShenandoahGC
-XX:ShenandoahGCMode=adaptive
-Xms8g
-Xmx8g
-jar uygulama.jar
GC Loglama ve Monitoring
GC’yi optimize etmek için önce ne olduğunu görmeniz gerekir. GC loglarını açmadan kör uçuş yapmazsınız.
# Java 9 ve sonrası için GC loglama
java
-Xms4g
-Xmx4g
-XX:+UseG1GC
-Xlog:gc*:file=/var/log/uygulama/gc.log:time,uptime,level,tags:filecount=5,filesize=20m
-jar uygulama.jar
# Java 8 için GC loglama (eski syntax)
java
-Xms4g
-Xmx4g
-XX:+UseG1GC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/var/log/uygulama/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=20M
-jar uygulama.jar
GC loglarını analiz etmek için GCEasy veya GCViewer gibi araçlar kullanabilirsiniz. Ama temel şeyleri komut satırından da görebilirsiniz:
# Çalışan JVM'in GC istatistiklerini anlık görmek
jstat -gcutil <PID> 1000 10
# Çıktı kolonları:
# S0: Survivor 0 doluluk yüzdesi
# S1: Survivor 1 doluluk yüzdesi
# E: Eden doluluk yüzdesi
# O: Old generation doluluk yüzdesi
# M: Metaspace doluluk yüzdesi
# YGC: Young GC sayısı
# YGCT: Young GC toplam süresi
# FGC: Full GC sayısı
# FGCT: Full GC toplam süresi
Full GC sıklığı yüksekse alarm verin. Birkaç dakikada bir Full GC yaşayan bir uygulama düzgün çalışmıyor demektir.
Gerçek Dünya Senaryosu: Spring Boot Uygulaması
Bir e-ticaret şirketinde çalıştığınızı düşünün. Spring Boot ile yazılmış ürün katalog servisi var. Günde 2 milyon request alıyor, response time SLA’sı 200ms. Ama peak saatlerde response time 2 saniyeyi geçiyor ve GC log’larında yoğun Full GC döngüleri görüyorsunuz.
Önce mevcut durumu teşhis edin:
# Çalışan uygulamanın heap dump'ını alın
jmap -dump:format=b,file=/tmp/heap-dump.hprof <PID>
# Veya JVM'e dump almayı OOM durumunda otomatik yaptırın
java
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/uygulama/heapdump.hprof
-jar katalog-servisi.jar
Heap dump’ı Eclipse MAT veya VisualVM ile analiz ettiğinizde, product nesnelerinin cache’e alınıp hiç temizlenmediğini, yani memory leak olduğunu fark edebilirsiniz. Kod düzeltmesi yapılırken kısa vadede şu konfigürasyonu uygulayabilirsiniz:
# Optimize edilmiş Spring Boot konfigürasyonu
java
-Xms2g
-Xmx2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1HeapRegionSize=8m
-XX:InitiatingHeapOccupancyPercent=35
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/uygulama/
-Xlog:gc*:file=/var/log/uygulama/gc.log:time,uptime:filecount=10,filesize=10m
-Dserver.port=8080
-jar katalog-servisi.jar
InitiatingHeapOccupancyPercent değerini 45’ten 35’e düşürmek, GC’nin daha erken başlamasını ve dolayısıyla daha kısa ama daha sık cycle’larla çalışmasını sağlar. Full GC’ye girmeden önce temizlik yapar.
Docker ve Kubernetes Ortamında JVM Ayarları
Container ortamında JVM ayarları çok daha kritiktir. JVM varsayılan olarak container limitlerini değil, host makinenin RAM’ini görür. Bu ciddi bir sorundur.
# Sorunlu durum: 2GB limitli container, host 32GB RAM
# JVM heap hesaplaması: 32GB * 0.25 = 8GB
# Container limit aşımı = OOMKilled
# Düzgün ayar: Container aware flags (Java 10+)
java
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0
-XX:+UseG1GC
-jar uygulama.jar
Container JVM parametreleri:
- UseContainerSupport: Container bellek limitlerini tanımasını sağlar (Java 10+ varsayılan açık)
- MaxRAMPercentage: Container belleğinin yüzde kaçını heap’e ayırsın
- InitialRAMPercentage: Başlangıç heap boyutu yüzdesi
Kubernetes deployment için örnek:
# Kubernetes ortamında JVM_OPTS environment variable ile yönetim
# deployment.yaml içinde:
# env:
# - name: JAVA_OPTS
# value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
# Dockerfile örneği
FROM eclipse-temurin:17-jre
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
COPY target/uygulama.jar /app/uygulama.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/uygulama.jar"]
JVM Tuning için Sistemsel Kontroller
Sadece JVM parametrelerine bakmak yetmez. OS seviyesinde de bazı şeylerin yerli yerinde olması gerekir.
# Swap kullanımını kontrol edin - JVM ve swap iyi anlaşmaz
free -h
cat /proc/swaps
# Swap'ı tamamen kapatmak (geçici)
swapoff -a
# Transparent Huge Pages ayarı - bazı GC'lerde sorun yaratır
cat /sys/kernel/mm/transparent_hugepage/enabled
# THP'yi devre dışı bırakmak
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# Kalıcı hale getirmek için /etc/rc.local veya systemd service kullanın
# Ulimit kontrolü - çok sayıda thread açan uygulamalar için
ulimit -u # max process/thread sayısı
ulimit -n # max open file descriptor
# Artırmak için /etc/security/limits.conf düzenleyin
echo "javauser soft nofile 65536" >> /etc/security/limits.conf
echo "javauser hard nofile 65536" >> /etc/security/limits.conf
Swap aktifken JVM çalıştırmak ciddi performans sorunlarına yol açar. GC sırasında JVM heap sayfalarının swap’a taşınması ve geri getirilmesi pause sürelerini saniyelerle ölçülür hale getirebilir.
JFR ve JMC ile Advanced Profiling
Java Flight Recorder (JFR), production’da düşük overhead’le profiling yapmanın en iyi yoludur. Java 11’den itibaren ücretsizdir.
# Çalışan uygulamada JFR kaydı başlatmak
jcmd <PID> JFR.start duration=120s filename=/tmp/kayit.jfr
# Kayıt durumunu kontrol etmek
jcmd <PID> JFR.check
# Kaydı durdurmak ve dosyaya yazmak
jcmd <PID> JFR.stop name=1 filename=/tmp/son-kayit.jfr
# Uygulama başlangıcından itibaren JFR aktif etmek
java
-XX:StartFlightRecording=duration=0,filename=/tmp/uygulama.jfr,settings=profile
-Xms4g
-Xmx4g
-XX:+UseG1GC
-jar uygulama.jar
JFR dosyasını Java Mission Control (JMC) ile açtığınızda GC pause’larını, memory allocation rate’ini, en çok nesne oluşturan kod yollarını ve thread sorunlarını tek bir arayüzden görebilirsiniz.
Prometheus ve Grafana ile GC Monitoring
Production’da sürekli elle GC durumuna bakamassınız. Micrometer veya JMX Exporter ile metrikleri Prometheus’a gönderin.
# JMX Exporter ile Prometheus entegrasyonu
# jmx_exporter.jar indirin ve yapılandırın
# config.yaml içeriği:
# rules:
# - pattern: 'java.lang<type=GarbageCollector, name=(.*)><>CollectionCount'
# - pattern: 'java.lang<type=GarbageCollector, name=(.*)><>CollectionTime'
# - pattern: 'java.lang<type=Memory><HeapMemoryUsage>used'
java
-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9090:/opt/jmx_exporter/config.yaml
-Xms4g
-Xmx4g
-XX:+UseG1GC
-jar uygulama.jar
# Prometheus endpoint kontrolü
curl http://localhost:9090/metrics | grep jvm_gc
Grafana’da izlemeniz gereken temel metrikler şunlardır:
- jvm_gc_pause_seconds: GC pause süreleri
- jvm_memory_used_bytes: Heap ve non-heap kullanımı
- jvm_memory_max_bytes: Maksimum bellek limitleri
- jvm_gc_memory_allocated_bytes: Allocation rate
- jvm_threads_live: Aktif thread sayısı
GC pause süresi 500ms’i geçtiğinde ve Full GC 5 dakikada bir olandan fazla yaşandığında alert kuralları tanımlayın.
Sık Yapılan Hatalar ve Çözümleri
Production’da en sık karşılaşılan JVM bellek sorunlarını ve çözümlerini derleyelim.
Hata 1: Heap boyutunu çok büyük ayarlamak
32GB heap vermek her zaman iyi değildir. Full GC yaşandığında 32GB’ın taranması dakikalar alabilir. Büyük heap yerine birden fazla JVM instance’ı çalıştırmayı düşünün.
Hata 2: -Xms ve -Xmx farklı ayarlamak
Başlangıçta küçük heap ile başlayıp büyüyen bir JVM, heap büyütme sırasında pause yaşar. Production’da ikisini eşit tutun.
Hata 3: Metaspace’i sınırsız bırakmak
MaxMetaspaceSize ayarlanmazsa class loader leak olan uygulamalar giderek artan Metaspace ile tüm sistem belleğini tüketir. Her zaman makul bir limit koyun.
Hata 4: GC loglarını açmamak
“Uygulama yavaşladı” şikayeti geldiğinde retrospektif analiz yapamazsınız. GC logları her zaman açık olmalı ve rotate edilmeli.
Hata 5: Container’da UseContainerSupport olmadan çalışmak
Java 8u191 öncesinde container support yoktu. Eski JVM versiyonlarında manuel olarak -Xmx ile limitleyin.
Performans Test Sonuçlarını Yorumlamak
Yaptığınız değişikliklerin işe yarayıp yaramadığını ölçmek için basit bir yaklaşım kullanın:
# GC istatistiklerini belirli aralıklarla kaydetmek
while true; do
jstat -gcutil $(pgrep -f uygulama.jar) >> /var/log/uygulama/gcstat.log
echo "---$(date)---" >> /var/log/uygulama/gcstat.log
sleep 60
done
# Toplam GC süresini hesaplamak
# FGCT (Full GC time) değerinin uptime'a oranı yüzde 1'in altında olmalı
# YGCT (Young GC time) değerinin oranı yüzde 5'in altında olmalı
Değişiklik öncesi ve sonrası bu değerleri karşılaştırmak, optimizasyonunuzun etkisini net olarak ortaya koyar.
Sonuç
JVM bellek optimizasyonu tek seferlik bir iş değil, sürekli izleme ve iyileştirme gerektiren bir süreçtir. Başlangıç noktanız her zaman mevcut durumu ölçmek olmalıdır. GC logları olmadan, profiling verileri olmadan yapılan “optimizasyon” genellikle sadece parametreleri tahmin yürüterek değiştirmekten ibarettir.
Özetlemek gerekirse: Production uygulamalarında G1GC veya ZGC ile başlayın, heap’i -Xms ve -Xmx eşit olacak şekilde RAM’in yüzde 70-75’ine ayarlayın, GC loglarını mutlaka açın ve Prometheus ile izleyin. Container ortamındaysanız UseContainerSupport ve MaxRAMPercentage parametrelerini kullanın. Swap’ı kapatın, THP’yi devre dışı bırakın ve Metaspace için makul bir üst limit belirleyin.
Her uygulama farklıdır ve bu yazıdaki değerler başlangıç noktalarıdır. Kendi uygulamanızın davranışını gözlemleyerek, GC loglarını okuyarak ve gerçek verilerle karar vererek en iyi konfigürasyona ulaşırsınız. İyi loglar, iyi monitoring ve sabırlı analiz, JVM tuning’in üç temel taşıdır.
