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.
