JMX ile Java Uygulama İzleme ve Yönetimi

Java uygulamalarını production ortamında izlemek, sorunları erkenden tespit etmek ve performans darboğazlarını bulmak için JMX (Java Management Extensions) vazgeçilmez bir araçtır. Ancak pek çok sysadmin JMX’i ya hiç kullanmıyor ya da sadece yüzeysel düzeyde tanıyor. Bu yazıda JMX’i gerçek dünya senaryolarıyla, hem geliştirici hem de sistem yöneticisi perspektifinden ele alacağız.

JMX Nedir ve Neden Önemlidir

JMX, Java SE platformunun bir parçası olarak gelen, Java uygulamalarını izlemek ve yönetmek için standart bir API’dir. 2003 yılında Java 5 ile birlikte platforma entegre edilmiştir. Ama sadece “izleme” demek yeterli değil; JMX ile uygulamayı yeniden başlatmadan konfigürasyon değiştirebilir, thread dump alabilir, heap analizi yapabilir ve özel metrikler toplayabilirsiniz.

Bir production sunucusunda JVM bellek kullanımı aniden artıyor, garbage collection süreleri uzuyor ya da thread sayısı patlamış durumda. İşte bu anlarda JMX sizin en yakın arkadaşınız oluyor. Prometheus, Grafana, Datadog gibi modern araçların altında da çoğunlukla JMX metrikleri yatıyor.

JMX’in temel bileşenleri:

  • MBean (Managed Bean): İzlenecek ya da yönetilecek kaynağı temsil eden Java nesnesi
  • MBean Server: MBean’lerin kayıt edildiği ve yönetildiği merkezi konteyner
  • JMX Agent: MBean Server’ı barındıran ve dışarıya açan katman
  • Connector/Adaptor: Uzaktan bağlantı için RMI, JMXMP gibi protokoller

JMX’i Etkinleştirme

Uygulama Başlatma Parametreleri

JMX’i bir Java uygulamasında aktifleştirmek için JVM parametrelerine birkaç satır eklemeniz yeterli:

java -Dcom.sun.management.jmxremote 
     -Dcom.sun.management.jmxremote.port=9999 
     -Dcom.sun.management.jmxremote.rmi.port=9998 
     -Dcom.sun.management.jmxremote.authenticate=false 
     -Dcom.sun.management.jmxremote.ssl=false 
     -Djava.rmi.server.hostname=192.168.1.100 
     -jar myapp.jar

Parametrelerin açıklaması:

  • jmxremote.port: JMX bağlantı portu, istemcilerin bağlanacağı yer
  • jmxremote.rmi.port: RMI callback portu, güvenlik duvarı arkasında sabit tutulması kritik
  • jmxremote.authenticate: Kimlik doğrulama açık/kapalı (production’da mutlaka true olmalı)
  • jmxremote.ssl: SSL şifreleme açık/kapalı (production’da mutlaka true olmalı)
  • java.rmi.server.hostname: Sunucunun dışarıya açık IP adresi, bu parametreyi atlamak en yaygın hata kaynaklarından biri

Kimlik Doğrulama ile Güvenli Kurulum

Production ortamında authentication olmadan JMX açmak büyük bir güvenlik açığıdır. Doğru şekilde yapılandıralım:

# JMX şifre dosyası oluştur
sudo mkdir -p /etc/java/jmx
sudo cp $JAVA_HOME/conf/management/jmxremote.password.template 
        /etc/java/jmxremote.password

# Dosyayı düzenle
sudo nano /etc/java/jmxremote.password
# monitorRole  monitor123
# controlRole  control456

# Dosya izinlerini düzelt (JMX bu olmadan çalışmaz)
sudo chmod 600 /etc/java/jmxremote.password
sudo chown appuser:appuser /etc/java/jmxremote.password
# Rol izinleri dosyası
sudo cp $JAVA_HOME/conf/management/jmxremote.access.template 
        /etc/java/jmxremote.access

# İçerik:
# monitorRole   readonly
# controlRole   readwrite

sudo chmod 644 /etc/java/jmxremote.access

Şimdi uygulamayı güvenli şekilde başlatın:

java -Dcom.sun.management.jmxremote 
     -Dcom.sun.management.jmxremote.port=9999 
     -Dcom.sun.management.jmxremote.rmi.port=9998 
     -Dcom.sun.management.jmxremote.authenticate=true 
     -Dcom.sun.management.jmxremote.ssl=false 
     -Dcom.sun.management.jmxremote.password.file=/etc/java/jmxremote.password 
     -Dcom.sun.management.jmxremote.access.file=/etc/java/jmxremote.access 
     -Djava.rmi.server.hostname=192.168.1.100 
     -jar myapp.jar

JConsole ve VisualVM ile Bağlanma

JConsole Kullanımı

JConsole, JDK ile birlikte gelen hazır GUI aracıdır. Hızlı bir bakış atmak için idealdir:

# Yerel process'e bağlanmak için
jconsole

# Uzak sunucuya bağlanmak için
jconsole 192.168.1.100:9999

# Kimlik doğrulama ile
jconsole -J-Djava.class.path=$JAVA_HOME/lib/jconsole.jar 
         service:jmx:rmi:///jndi/rmi://192.168.1.100:9999/jmxrmi

JConsole açıldığında şu sekmeler sizi karşılar:

  • Overview: Heap, thread, sınıf ve CPU kullanımının anlık görünümü
  • Memory: Heap ve non-heap bellek detayları, GC istatistikleri
  • Threads: Aktif thread listesi, deadlock tespiti
  • Classes: Yüklü sınıf sayısı
  • VM Summary: JVM parametreleri ve sistem bilgisi
  • MBeans: Tüm MBean’lere erişim

Komut Satırından JMX: jmxterm

GUI olmayan sunucularda jmxterm aracı hayat kurtarır:

# jmxterm indir
wget https://github.com/jiaqi/jmxterm/releases/download/v1.0.4/jmxterm-1.0.4-uber.jar

# Bağlan
java -jar jmxterm-1.0.4-uber.jar

# İçinde komutlar:
open 192.168.1.100:9999
domains
beans
bean java.lang:type=Memory
info
get HeapMemoryUsage

Özel MBean Yazma

Kendi uygulamanız için özel metrikler toplamak istediğinizde MBean yazmanız gerekir. Gerçek bir senaryo üzerinden gidelim: Bir e-ticaret uygulamasında aktif sipariş sayısını ve işlem süresini izlemek istiyorsunuz.

# MBean interface tanımla (OrderManagerMBean.java)
cat > OrderManagerMBean.java << 'EOF'
public interface OrderManagerMBean {
    int getActiveOrderCount();
    long getAverageProcessingTimeMs();
    int getTotalProcessedOrders();
    void resetStatistics();
    String getStatus();
}
EOF
# MBean implementasyonu (OrderManager.java)
cat > OrderManager.java << 'EOF'
import java.lang.management.ManagementFactory;
import javax.management.*;
import java.util.concurrent.atomic.*;

public class OrderManager implements OrderManagerMBean {
    
    private AtomicInteger activeOrders = new AtomicInteger(0);
    private AtomicLong totalProcessingTime = new AtomicLong(0);
    private AtomicInteger totalProcessed = new AtomicInteger(0);
    
    public OrderManager() {
        try {
            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            ObjectName name = new ObjectName(
                "com.myapp:type=OrderManager"
            );
            mbs.registerMBean(this, name);
            System.out.println("OrderManager MBean kayıt edildi.");
        } catch (Exception e) {
            throw new RuntimeException("MBean kayıt hatası", e);
        }
    }
    
    @Override
    public int getActiveOrderCount() {
        return activeOrders.get();
    }
    
    @Override
    public long getAverageProcessingTimeMs() {
        int count = totalProcessed.get();
        return count == 0 ? 0 : totalProcessingTime.get() / count;
    }
    
    @Override
    public int getTotalProcessedOrders() {
        return totalProcessed.get();
    }
    
    @Override
    public void resetStatistics() {
        totalProcessingTime.set(0);
        totalProcessed.set(0);
        System.out.println("İstatistikler sıfırlandı.");
    }
    
    @Override
    public String getStatus() {
        return activeOrders.get() > 100 ? "YÜKSEK_YÜK" : "NORMAL";
    }
    
    // İş mantığı metodları
    public void orderReceived() { activeOrders.incrementAndGet(); }
    
    public void orderCompleted(long processingTimeMs) {
        activeOrders.decrementAndGet();
        totalProcessed.incrementAndGet();
        totalProcessingTime.addAndGet(processingTimeMs);
    }
}
EOF

Bu yapı sayesinde JConsole’dan ya da herhangi bir JMX istemcisinden resetStatistics() metodunu çağırabilir, uygulamayı durdurmadan istatistikleri sıfırlayabilirsiniz.

JVM Metriklerini Komut Satırından Okuma

jcmd ile Hızlı Tanı

Modern Java sürümlerinde jcmd komutu çok güçlüdür:

# Çalışan Java process'lerini listele
jcmd -l

# Heap bilgisi al
jcmd <PID> VM.native_memory

# Thread dump al
jcmd <PID> Thread.print > /tmp/threaddump-$(date +%Y%m%d-%H%M%S).txt

# GC istatistikleri
jcmd <PID> GC.heap_info

# JVM flags'i görüntüle
jcmd <PID> VM.flags

# Heap dump al (dikkatli kullanın, uygulama durur)
jcmd <PID> GC.heap_dump /tmp/heapdump-$(date +%Y%m%d).hprof

jstat ile GC İzleme

GC sorunlarını gerçek zamanlı takip etmek için jstat vazgeçilmezdir:

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

# Daha detaylı GC bilgisi, 2 saniyede bir, 30 kez
jstat -gc <PID> 2000 30

# GC neden olduğu duraklamalar
jstat -gccause <PID> 1000

# Sınıf yükleme istatistikleri
jstat -class <PID> 1000

jstat -gcutil çıktısını yorumlamak:

  • S0, S1: Survivor space kullanım yüzdesi
  • E: Eden space kullanım yüzdesi (hızlı artıyorsa bellek sızıntısı olabilir)
  • O: Old generation kullanım yüzdesi (sürekli artıyorsa ciddi sorun var)
  • M: Metaspace kullanım yüzdesi
  • YGC: Young generation GC sayısı
  • FGC: Full GC sayısı (bu yüksekse kritik)
  • FGCT: Full GC toplam süresi

Prometheus ile JMX Metriklerini Toplama

Modern monitoring stack’lerinde JMX metriklerini Prometheus’a aktarmak için jmx_exporter kullanılır. Bu gerçek dünya senaryosu en sık karşılaşacağınız kurulum:

# jmx_exporter indir
wget https://repo1.maven.org/maven2/io/prometheus/jmx/
jmx_prometheus_javaagent/0.19.0/
jmx_prometheus_javaagent-0.19.0.jar 
-O /opt/jmx-exporter/jmx_prometheus_javaagent.jar

# Konfigürasyon dosyası oluştur
cat > /opt/jmx-exporter/config.yml << 'EOF'
---
startDelaySeconds: 0
ssl: false
lowercaseOutputName: false
lowercaseOutputLabelNames: false

rules:
  # JVM Memory
  - pattern: 'java.lang<type=Memory><>(w+)MemoryUsage'
    name: jvm_memory_$1_bytes
    type: GAUGE
    attrNameSnakeCase: true

  # GC istatistikleri
  - pattern: 'java.lang<type=GarbageCollector,name=(.*)><>CollectionCount'
    name: jvm_gc_collection_count
    type: COUNTER
    labels:
      gc: "$1"

  # Thread sayısı
  - pattern: 'java.lang<type=Threading><>ThreadCount'
    name: jvm_thread_count
    type: GAUGE

  # Özel MBean metrikleri
  - pattern: 'com.myapp<type=OrderManager><>(w+)'
    name: app_order_$1
    type: GAUGE
EOF
# Uygulamayı jmx_exporter agent ile başlat
java -javaagent:/opt/jmx-exporter/jmx_prometheus_javaagent.jar=
8080:/opt/jmx-exporter/config.yml 
     -jar myapp.jar

# Metrikleri kontrol et
curl http://localhost:8080/metrics | grep jvm_memory

Tomcat ve Spring Boot için JMX

Tomcat JMX Yapılandırması

# catalina.sh içine ekleyin ya da setenv.sh oluşturun
cat > $CATALINA_HOME/bin/setenv.sh << 'EOF'
CATALINA_OPTS="$CATALINA_OPTS 
  -Dcom.sun.management.jmxremote 
  -Dcom.sun.management.jmxremote.port=9999 
  -Dcom.sun.management.jmxremote.rmi.port=9998 
  -Dcom.sun.management.jmxremote.authenticate=true 
  -Dcom.sun.management.jmxremote.ssl=false 
  -Dcom.sun.management.jmxremote.password.file=/etc/tomcat/jmxremote.password 
  -Dcom.sun.management.jmxremote.access.file=/etc/tomcat/jmxremote.access 
  -Djava.rmi.server.hostname=$(hostname -I | awk '{print $1}')"
EOF

chmod +x $CATALINA_HOME/bin/setenv.sh

Spring Boot Actuator ve JMX Entegrasyonu

Spring Boot uygulamalarında application.properties üzerinden JMX kontrolü yapılır:

cat > src/main/resources/application.properties << 'EOF'
# JMX etkinleştir
spring.jmx.enabled=true
spring.jmx.default-domain=myapp

# Actuator endpoint'leri
management.endpoints.jmx.exposure.include=health,info,metrics,env
management.endpoint.health.show-details=always

# JMX over HTTP (Actuator)
management.server.port=8081
management.endpoints.web.exposure.include=*
EOF

Otomatik Uyarı ve Monitoring Scripti

Sysadmin olarak JMX metriklerini izleyip alarm üretecek bir script işinizi çok kolaylaştırır:

#!/bin/bash
# jmx-monitor.sh - JMX metrik kontrol scripti

PID=$(pgrep -f "myapp.jar")
HEAP_THRESHOLD=80
GC_TIME_THRESHOLD=10
ALERT_EMAIL="[email protected]"
LOG_FILE="/var/log/jmx-monitor.log"

check_heap_usage() {
    local heap_info=$(jcmd $PID GC.heap_info 2>/dev/null)
    local used=$(echo "$heap_info" | grep -oP 'used K[0-9]+')
    local capacity=$(echo "$heap_info" | grep -oP 'capacity K[0-9]+')
    
    if [ -n "$used" ] && [ -n "$capacity" ]; then
        local usage_pct=$((used * 100 / capacity))
        echo "$(date): Heap kullanımı: %$usage_pct" >> $LOG_FILE
        
        if [ $usage_pct -gt $HEAP_THRESHOLD ]; then
            echo "UYARI: Heap kullanımı %$usage_pct seviyesinde!" | 
                mail -s "[ALARM] Yüksek Heap Kullanımı - $(hostname)" $ALERT_EMAIL
            
            # Otomatik thread dump al
            jcmd $PID Thread.print > 
                /tmp/threaddump-alarm-$(date +%Y%m%d-%H%M%S).txt
        fi
    fi
}

check_full_gc() {
    local fgc_before=$(jstat -gc $PID 1 1 | tail -1 | awk '{print $15}')
    sleep 60
    local fgc_after=$(jstat -gc $PID 1 1 | tail -1 | awk '{print $15}')
    local fgc_diff=$((fgc_after - fgc_before))
    
    echo "$(date): Son 1 dk Full GC sayısı: $fgc_diff" >> $LOG_FILE
    
    if [ $fgc_diff -gt $GC_TIME_THRESHOLD ]; then
        echo "UYARI: 1 dakikada $fgc_diff Full GC tespit edildi!" | 
            mail -s "[ALARM] Yoğun Full GC - $(hostname)" $ALERT_EMAIL
    fi
}

if [ -z "$PID" ]; then
    echo "$(date): HATA - myapp.jar process bulunamadı!" >> $LOG_FILE
    echo "Java uygulaması çalışmıyor!" | 
        mail -s "[ALARM] Uygulama DOWN - $(hostname)" $ALERT_EMAIL
    exit 1
fi

echo "$(date): PID $PID için kontrol başlıyor..." >> $LOG_FILE
check_heap_usage
check_full_gc

echo "$(date): Kontrol tamamlandı." >> $LOG_FILE
# Scripti cron'a ekle (her 5 dakikada bir çalışsın)
chmod +x /usr/local/bin/jmx-monitor.sh
echo "*/5 * * * * appuser /usr/local/bin/jmx-monitor.sh" | crontab -

Sık Karşılaşılan Sorunlar ve Çözümleri

“Connection refused” hatası alıyorsunuz:

Bu genellikle java.rmi.server.hostname parametresinin eksik ya da yanlış olmasından kaynaklanır. Sunucu birden fazla ağ arayüzüne sahipse doğru IP’yi belirtmek zorundasınız. Güvenlik duvarında hem JMX port’unu hem de RMI port’unu açmayı unutmayın.

# Portların açık olup olmadığını kontrol et
ss -tlnp | grep -E "9998|9999"

# Güvenlik duvarı kuralları (firewalld)
firewall-cmd --add-port=9998/tcp --permanent
firewall-cmd --add-port=9999/tcp --permanent
firewall-cmd --reload

MBean değerleri güncellenmiyor:

MBean Server’ın doğru domain’de kayıtlı olup olmadığını kontrol edin. JConsole’da “MBeans” sekmesine girip kendi domain’inizi göremiyor musunuz? ObjectName formatını gözden geçirin, çok sık yapılan hata özel karakterlerin kaçırılmamasıdır.

JMX bağlantısı geliyor ama metrikler boş geliyor:

Spring Boot uygulamalarında spring.jmx.enabled=false varsayılan değeri Spring Boot 2.x sonrasında değişti, özellikle kontrol etmeniz gerekiyor.

Sonuç

JMX, Java dünyasında izleme ve yönetim için standart ve güçlü bir altyapıdır. Temel JVM metriklerinden özel uygulama istatistiklerine kadar her şeyi hem anlık hem de uzaktan yönetmenizi sağlar. Bir sysadmin olarak bakıldığında, JMX’i iyi bilmek sizi production sorunlarında çok daha hızlı tepki verebilir hale getirir.

Pratik olarak yapmanız gerekenler özetle şöyledir: Önce java.rmi.server.hostname ve çift port yapılandırmasını doğru yapın, production’da mutlaka kimlik doğrulama açın. Ardından Prometheus jmx_exporter ile metrikleri Grafana’ya taşıyın ve anlamlı alertler kurun. Kendi uygulamanıza özel MBean’ler yazarak iş seviyesindeki metrikleri de aynı altyapıyla takip edin.

JMX başlangıçta karmaşık görünse de temel kavramları oturduktan sonra “neden bunu daha önce kullanmadım” diyeceğinizden eminim. Özellikle gece 3’te bellek sızıntısı avlarken JMX’in değerini çok daha iyi anlayacaksınız.

Bir yanıt yazın

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