Linux’ta JVM Performans Sorunlarını Tespit Etme

Production ortamında bir Java uygulaması yavaşlamaya başladığında, ekip lideri sana bakıyor ve “ne olduğunu bul” diyor. İşte o an için bu yazıyı hazırladım. JVM performans sorunları sinsi olur; bazen heap dolup taşar, bazen GC çılgına döner, bazen thread’ler birbirini bekler. Doğru araçları ve yaklaşımı bilmeden bu labirentin içinde kaybolmak çok kolaydır.

JVM Performans Sorunlarının Temel Nedenleri

Sorunu bulmadan önce ne arayacağını bilmen gerekiyor. JVM tabanlı uygulamalarda performans düşüşü genellikle şu dört kategoriden birinde gizlenir:

  • Memory Leak veya Heap Şişmesi: Nesneler garbage collector tarafından toplanamıyor, heap dolup taşıyor.
  • GC Baskısı: Garbage Collector çok sık çalışıyor ve uygulamayı durduruyor (Stop-The-World pause).
  • Thread Sorunları: Deadlock, thread açlığı veya aşırı thread oluşturma.
  • CPU Spike’ları: Sonsuz döngüler, verimsiz algoritmalar veya JIT derleme sorunları.

Bu kategorilerin her birini tespit etmek için farklı araçlar kullanacağız.

İlk Adım: Sistemin Genel Sağlığını Kontrol Et

Hemen JVM’e dalmadan önce, işletim sistemi seviyesinde neler olduğuna bir bak. Çoğu zaman sorun sandığından daha yüzeysel olabilir.

# JVM process ID'sini bul
ps aux | grep java

# Daha temiz bir çıktı için
pgrep -la java

# Tek bir PID ile detaylı bakış
top -p <PID>

# Gerçek zamanlı CPU ve bellek kullanımı
pidstat -u -r -p <PID> 1 5

pidstat çıktısında %CPU sürekli yüksekse bir yerlerde CPU’yu yiyen bir şey var demektir. %MEM artış gösteriyorsa memory leak şüphesi doğar.

Bir de dosya tanımlayıcılarına bak. Uzun süre çalışan Java uygulamaları zaman zaman dosya veya socket descriptor sızdırır:

# Açık dosya tanımlayıcı sayısı
ls -la /proc/<PID>/fd | wc -l

# Sistemdeki limit
cat /proc/<PID>/limits | grep "open files"

# Hangi dosyalar açık?
lsof -p <PID> | head -50

Eğer açık FD sayısı limite yaklaşıyorsa, uygulamanın önce ya kaynak sızdırdığını incele ya da limiti artır.

jstat ile GC Performansını İzle

jstat JDK ile gelen hafif ama güçlü bir araçtır. GC istatistiklerini canlı olarak izlemeni sağlar. Bunu kullanmak için JVM’in aynı kullanıcı altında çalışması veya root yetkisi gerekir.

# Her 1 saniyede bir GC istatistiklerini göster (20 kez)
jstat -gcutil <PID> 1000 20

# Daha detaylı GC kapasite bilgisi
jstat -gc <PID> 2000

# GC nedeniyle geçen toplam süre
jstat -gccause <PID> 1000

jstat -gcutil çıktısını yorumlamak için şunlara dikkat et:

  • S0, S1: Survivor space doluluk yüzdesi. Sürekli %100 ise nesneler Old Gen’e çok hızlı terfi ediyor.
  • E: Eden space. Hızlı dolup boşalıyorsa Minor GC sık çalışıyor demektir.
  • O: Old Generation doluluk yüzdesi. Bu yavaş yavaş artıyor ve hiç düşmüyorsa memory leak var.
  • FGC: Full GC sayısı. Bu sayı hızlı artıyorsa ciddi sorun var.
  • FGCT: Full GC’de harcanan toplam süre. Saniyeler mertebesine çıkmışsa kullanıcılar bunu hissediyordur.

Gerçek dünya senaryosu olarak şunu düşün: Production’da bir e-ticaret uygulaması her gece belirli bir saatte yavaşlıyor. jstat -gcutil çıktısına baktığında O (Old Gen) sütununun gün boyunca %20’den %95’e çıktığını görüyorsun. Bu klasik bir memory leak işareti.

Heap Dump Alma ve Analiz Etme

Memory leak şüphesi varsa heap dump almak şarttır. Bunu yaparken dikkatli ol; heap dump alma işlemi uygulamayı kısa süreliğine durdurur.

# jmap ile heap dump al
jmap -dump:format=b,file=/tmp/heapdump.hprof <PID>

# Sadece live nesneleri dump et (daha küçük dosya, daha temiz analiz)
jmap -dump:live,format=b,file=/tmp/heapdump_live.hprof <PID>

# Kısa heap özeti (dump almadan)
jmap -histo <PID> | head -30

# Live nesnelerle sınırlı histogram
jmap -histo:live <PID> | head -30

jmap -histo çıktısı çok değerlidir. Hangi sınıftan kaç nesne olduğunu ve ne kadar bellek kapladığını gösterir. Eğer byte[] veya char[] dizileri listenin tepesinde çok büyük boyutlarda görünüyorsa, büyük ihtimalle String tabanlı bir sızıntı var demektir. Eğer kendi uygulama sınıflarından biri beklenmedik miktarda instance varsa, o sınıfı yakından incelemelisin.

Heap dump dosyasını analiz etmek için Eclipse Memory Analyzer Tool (MAT) ya da VisualVM kullanabilirsin. MAT’i sunucuya kurmak yerine, dump dosyasını yerel makineye kopyalayıp analiz etmek daha pratiktir:

# Heap dump'ı yerel makinene kopyala
scp user@server:/tmp/heapdump_live.hprof ~/Desktop/

# MAT ile açmak için (MAT kuruluysa)
./MemoryAnalyzer /root/Desktop/heapdump_live.hprof

Thread Dump Alma ve Analiz Etme

Uygulama donuyor, request’ler yanıt vermiyor ama bellek ve CPU normal görünüyorsa, thread sorunu ihtimali yüksektir. Thread dump almak uygulamayı durdurmaz ve anlık bir fotoğraf çeker.

# kill -3 ile thread dump (stdout/log'a yazar)
kill -3 <PID>

# jstack ile thread dump
jstack <PID> > /tmp/threaddump_$(date +%Y%m%d_%H%M%S).txt

# Daha detaylı, kilit bilgisiyle
jstack -l <PID> > /tmp/threaddump_locks.txt

# Birden fazla dump alarak karşılaştır (30 saniye arayla)
for i in 1 2 3; do
  jstack <PID> > /tmp/td_$i.txt
  echo "Dump $i alındı"
  sleep 30
done

Thread dump analizinde şunlara bak:

  • BLOCKED durumundaki thread’ler: Bir monitörü almak için başka bir thread’i bekliyorlar. Deadlock işareti olabilir.
  • WAITING veya TIMED_WAITING: Genellikle normaldir ama çok fazlaysa thread pool açlığı olabilir.
  • Aynı stack trace’i olan çok sayıda thread: Bir bottleneck noktasının işareti.

jstack çıktısında deadlock varsa, JVM bunu açıkça raporlar:

grep -A 20 "deadlock" /tmp/threaddump_locks.txt

Deadlock senaryosu şöyle görünür: Thread A, Lock X’i tutarken Lock Y’yi bekliyor. Thread B, Lock Y’yi tutarken Lock X’i bekliyor. İkisi de ilerleyemiyor. Bu durumda genellikle kod seviyesinde bir düzeltme gerekir.

CPU Yiyen Thread’i Bulmak

“Java uygulaması CPU’yu %100 kullanıyor” şikayeti alıyorsun. Hangi thread’in suçlu olduğunu bulmak için şu yöntemi kullan:

# En çok CPU kullanan thread'leri bul
top -H -p <PID>

# ps ile de görebilirsin
ps -mo pid,lwp,%cpu,comm -p <PID> | sort -rk3 | head -20

top -H -p çıktısında en üstteki thread’lerin LWP (Light Weight Process) ID’sini not al. Bu ID’yi ondalıktan onaltılığa çevir ve thread dump’ta ara:

# Örneğin LWP ID 12345 ise
printf '%xn' 12345
# Çıktı: 3039

# Thread dump'ta bu ID'yi ara
grep -A 15 "nid=0x3039" /tmp/threaddump_locks.txt

Bu şekilde tam olarak hangi kodun CPU’yu tükettiğini stack trace üzerinden görebilirsin. Gerçek hayatta bu yöntemi kullanarak bir uygulamada JSON serialization içinde sonsuz döngüye giren bir custom serializer buldum. Thread dump olmadan bulmak saatler alırdı.

JVM Bayraklarını ve GC Loglarını İncelemek

Çalışan JVM’in hangi bayraklarla başlatıldığını görmek çok işe yarar:

# Tüm JVM bayraklarını göster
jcmd <PID> VM.flags

# Komut satırı argümanlarını göster
jcmd <PID> VM.command_line

# Sistem özelliklerini listele
jcmd <PID> VM.system_properties

# Çalışma süresi istatistikleri
jcmd <PID> VM.uptime

GC logları ise altın değerinde bilgi içerir. Eğer uygulama GC loglama ile başlatılmamışsa, runtime’da etkinleştirme seçeneğin sınırlıdır. Ama varsa analiz et:

# GC log dosyasını canlı izle
tail -f /var/log/app/gc.log

# Full GC olaylarını filtrele
grep -i "full gc" /var/log/app/gc.log | tail -20

# GC pause sürelerini çek (G1GC formatı)
grep "Pause" /var/log/app/gc.log | awk '{print $NF}' | sort -n | tail -10

Modern JVM’lerde (Java 11+) Unified GC Logging formatı kullanılır. GC log analizini kolaylaştırmak için GCViewer veya GCEasy gibi araçlar kullanabilirsin.

jcmd ile Kapsamlı Tanılama

jcmd aracı tek bir araçla pek çok tanılama görevini yapmanı sağlar:

# Tüm Java process'lerini listele
jcmd -l

# Kullanılabilir komutları gör
jcmd <PID> help

# Native bellek kullanım raporu (NMT etkinse)
jcmd <PID> VM.native_memory summary

# Heap bilgisi özeti
jcmd <PID> GC.heap_info

# Class loading istatistikleri
jcmd <PID> VM.class_stats 2>/dev/null | head -20

# Anlık heap histogram
jcmd <PID> GC.class_histogram | head -30

# Thread dump (jstack alternatifi)
jcmd <PID> Thread.print > /tmp/threaddump_jcmd.txt

# GC tetikle (test amaçlı)
jcmd <PID> GC.run

Native Memory Tracking (NMT) özellikle “bellek sızdırıyor ama heap’te göremiyorum” durumlarında hayat kurtarır. Off-heap bellek kullanımını izler. Bunun için JVM’in -XX:NativeMemoryTracking=summary veya detail ile başlatılmış olması gerekir.

Performans Profiling: async-profiler

Production’da JVM profiler kullanmak ciddi overhead getirir. Bu yüzden sampling tabanlı async-profiler tercih edilir. Hem CPU hem de bellek profillemesi yapabilir, hem de JVM’e düşük etki bırakır.

# async-profiler indir
wget https://github.com/async-profiler/async-profiler/releases/latest/download/async-profiler-3.0-linux-x64.tar.gz
tar xzf async-profiler-3.0-linux-x64.tar.gz
cd async-profiler-3.0-linux-x64

# 30 saniye CPU profili al ve flame graph oluştur
./bin/asprof -d 30 -f /tmp/flamegraph.html <PID>

# Sadece allokasyon profili
./bin/asprof -e alloc -d 30 -f /tmp/alloc.html <PID>

# Wall-clock profili (IO bekleyen thread'ler dahil)
./bin/asprof -e wall -d 30 -f /tmp/wall.html <PID>

Flame graph çıktısını tarayıcıda açtığında, en geniş dikdörtgenler en çok zaman harcanan metodları gösterir. Tepedeki geniş bir blok, o metodun bottleneck olduğuna işaret eder. Bu analiz sayesinde “hangisi yavaş?” sorusunun cevabını dakikalar içinde bulursun.

Otomatik İzleme: Prometheus JMX Exporter

Reaktif tanılama yerine proaktif izleme daha iyidir. JMX Exporter, JVM metriklerini Prometheus’a aktarır. Grafana ile görselleştirince anlık dalgalanmalar yerine trendleri takip edebilirsin.

# JMX Exporter jar indir
wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.20.0/jmx_prometheus_javaagent-0.20.0.jar

# Basit config dosyası oluştur
cat > /opt/jmx_exporter/config.yml << 'EOF'
rules:
  - pattern: ".*"
EOF

# Java uygulamasını agent ile başlat
java -javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent-0.20.0.jar=9090:/opt/jmx_exporter/config.yml 
     -jar /opt/myapp/application.jar

# Metrikleri kontrol et
curl http://localhost:9090/metrics | grep -E "jvm_gc|jvm_memory|jvm_threads" | head -20

Bu şekilde Grafana dashboard’unda heap kullanımı, GC pause süresi ve thread sayısı gibi metrikleri canlı izleyebilirsin. Bir alert kuralı ekleyerek Old Gen %80’i geçtiğinde Slack’e bildirim gönderebilirsin. O zaman “uygulama yavaşladı” şikayeti sana geldiğinde, elinde zaten geçmiş veri olur.

Hızlı Tanılama Kontrol Listesi

Bir JVM performans sorunu geldiğinde şu sırayla hareket et:

  • Önce ps ve top ile CPU/bellek genel durumuna bak
  • jstat -gcutil ile GC durumunu kontrol et
  • CPU yüksekse top -H ile hangi thread’in suçlu olduğunu bul ve jstack ile eşleştir
  • Memory artıyorsa jmap -histo:live ile en büyük nesneleri gör
  • Uygulama donmuşsa birden fazla thread dump alıp karşılaştır
  • jcmd ile JVM bayraklarını ve heap özetini kontrol et
  • Derin analiz için async-profiler ile flame graph al

Sonuç

JVM performans sorunları karmaşık görünse de doğru araçlarla sistematik bir şekilde yaklaşınca çözülmesi mümkündür. jstat, jstack, jmap ve jcmd dörtlüsü JDK ile birlikte gelir ve ek kurulum gerektirmez; bu yüzden production ortamında her zaman kullanılabilirdir. async-profiler ise overhead’i düşük tutarak derinlemesine profiling yapmanı sağlar.

En önemli nokta şu: Sorun yaşandığında araç aramaya başlamamak, onları önceden hazır bulundurmak. GC loglama açık olsun, JMX Exporter çalışıyor olsun, heap dump dizini yazılabilir olsun. O şekilde gece yarısı production alarmı geldiğinde saatlerce araç kurmak yerine doğrudan problemi çözmeye odaklanırsın. Sistem yöneticiliğinin yarısı teknik bilgi, diğer yarısı hazırlıklı olmaktır.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir