JMX and MXBeans: JVM Hotspot Diagnostics and Custom MBeans
Learn how to use JMX and MXBeans to monitor JVM memory pools, perform hotspot diagnostics, and build custom MBeans for production observability.
JMX and MXBeans: JVM Hotspot Diagnostics and Custom MBeans
Java Management Extensions (JMX) is the standard Java platform technology for monitoring and managing applications at runtime. Every JVM ships with a set of built-in MXBeans that expose critical runtime data: heap memory usage, garbage collection statistics, thread counts, CPU utilization, and class loading metrics. Beyond the built-in beans, JMX lets you expose your own application metrics through custom MBeans.
This guide walks through the JMX architecture, the built-in MXBeans you will actually use, how to write custom MBeans without shooting yourself in the foot, and the gotchas that bite people in production.
Introduction
Java Management Extensions (JMX) is the standard Java platform technology for monitoring and managing applications at runtime. Every JVM ships with a set of built-in MXBeans that expose critical runtime data: heap memory usage, garbage collection statistics, thread counts, CPU utilization, and class loading metrics. If you are running a Java application in production and not using JMX, you are flying blind—you have no visibility into the JVM’s internal state, no way to detect memory leaks before they cause outages, and no mechanism to diagnose why a service is consuming more CPU than expected.
Beyond the built-in beans that the JVM provides automatically, JMX lets you expose your own application metrics through custom MBeans. A well-designed MBean gives operators and monitoring systems a standardized interface to inspect queue depths, cache hit rates, request latencies, and any other business-level metric you care about. The JMX architecture is deliberately simple: MBeans register with an MBean server, and clients (JConsole, VisualVM, Prometheus exporters, custom tooling) connect to that server to read attributes and invoke operations. This simplicity means JMX integration works consistently across all JVM-based applications without application-specific code.
This guide walks through the JMX architecture, the built-in MXBeans you will actually use, how to write custom MBeans without shooting yourself in the foot, and the failure modes that bite people in production. You will learn which MXBeans to poll for memory and GC health, how to create custom metrics that integrate with Prometheus via the JMX exporter, and why you should never expose JMX remotely without authentication. By the end, you will be able to build a monitoring strategy that gives you real visibility into JVM behavior and application health.
What is JMX?
JMX is the Java EE management specification that provides a standardized way to expose runtime metrics and control operations. The architecture has three layers:
- Instrumentation layer: MBeans (Managed Beans) that expose attributes and operations
- Agent layer: The MBean server that registers and hosts MBeans
- Distributed layer: Connectors and adapters that expose the MBean server to remote clients
In practice, every JVM automatically starts an MBean server with a set of platform MXBeans. You access them through JConsole, VisualVM, or any JMX client.
When to Use JMX and MXBeans
Ideal Use Cases
- Runtime metrics collection: Pulling heap memory, GC stats, thread counts into your monitoring dashboards
- Application health checks: Exposing business-level metrics like queue depths, cache hit rates, or request counts
- Remote diagnostics: Connecting to a running JVM to inspect state without restarting
- Dynamic configuration: Changing application behavior at runtime through JMX operations
- Alerting integration: Exporting JMX metrics to Prometheus, Datadog, or Grafana via JMX exporters
When NOT to Rely Solely on JMX
- High-frequency metrics: JMX polling adds overhead if you sample every second across many attributes
- Distributed tracing: JMX has no concept of request traces; use OpenTracing or Micrometer for that
- Production incident response: JMX is pull-based; you cannot see what happened before you connected
- Low-latency code paths: Reading certain MXBean attributes (like
MemoryPoolallocations) can trigger safepoints
Architecture
The JMX ecosystem connects several components:
graph TB
subgraph "JVM Process"
MServer[MBean Server]
subgraph "Platform MXBeans"
MEM[Memory MXBean]
GC[GarbageCollector MXBean]
TH[ThreadMXBean]
CL[ClassLoadingMXBean]
RT[RuntimeMXBean]
CP[CompilationMXBean]
end
subgraph "Application MBeans"
CM[Custom MBean]
CM2[Custom MBean]
end
end
subgraph "Remote Access"
JC[JConsole]
VV[VisualVM]
PJ[Prometheus JMX Exporter]
NB[Native JMX Client]
end
MServer <-->|RMI/Agent| JC
MServer <-->|RMI/Agent| VV
MServer <-->|HTTP/Agent| PJ
MServer <-->|RMI| NB
CM --> MServer
CM2 --> MServer
MEM --> MServer
GC --> MServer
TH --> MServer
CL --> MServer
RT --> MServer
CP --> MServer
Built-in Platform MXBeans
| MXBean | ObjectName | Key Attributes |
|---|---|---|
| MemoryMXBean | java.lang:type=Memory | HeapMemoryUsage, NonHeapMemoryUsage, ObjectPendingFinalizationCount |
| GarbageCollectorMXBean | java.lang:type=GarbageCollector,name=* | CollectionCount, CollectionTime, MemoryPoolNames |
| ThreadMXBean | java.lang:type=Threading | ThreadCount, PeakThreadCount, DaemonThreadCount, DeadlockedThreads |
| ClassLoadingMXBean | java.lang:type=ClassLoading | LoadedClassCount, TotalLoadedClassCount, UnloadedClassCount |
| RuntimeMXBean | java.lang:type=Runtime | Uptime, VmName, VmVersion, InputArguments |
| CompilationMXBean | java.lang:type=Compilation | Name, CompilerTotalTime |
| OperatingSystemMXBean | java.lang:type=OperatingSystem | AvailableProcessors, Arch, OSVersion, SystemLoadAverage |
Implementation
Accessing Built-in MXBeans
import java.lang.management.*;
public class BuiltInMXBeanAccess {
public void printMemoryStats() {
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heap = memoryMXBean.getHeapMemoryUsage();
System.out.println("Heap Memory:");
System.out.println(" Used: " + heap.getUsed() / 1024 / 1024 + " MB");
System.out.println(" Max: " + heap.getMax() / 1024 / 1024 + " MB");
System.out.println(" Committed: " + heap.getCommitted() / 1024 / 1024 + " MB");
}
public void printGCStats() {
List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gc : gcBeans) {
System.out.println("GC: " + gc.getName());
System.out.println(" Collections: " + gc.getCollectionCount());
System.out.println(" Time: " + gc.getCollectionTime() + " ms");
System.out.println(" Pools: " + gc.getMemoryPoolNames());
}
}
public void printThreadStats() {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
System.out.println("Threads:");
System.out.println(" Current: " + threadMXBean.getThreadCount());
System.out.println(" Peak: " + threadMXBean.getPeakThreadCount());
System.out.println(" Daemon: " + threadMXBean.getDaemonThreadCount());
System.out.println(" Total started: " + threadMXBean.getTotalStartedThreadCount());
// Find deadlocks
long[] deadlocks = threadMXBean.findDeadlockedThreads();
if (deadlocks != null && deadlocks.length > 0) {
System.out.println(" DEADLOCKED: " + deadlocks.length + " threads!");
}
}
public void detectMemoryLeaks() {
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heap = memoryMXBean.getHeapMemoryUsage();
double usedRatio = (double) heap.getUsed() / heap.getMax();
if (usedRatio > 0.9) {
System.err.println("WARNING: Heap usage above 90%!");
}
// Check for memory pool issues
List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean pool : pools) {
MemoryUsage usage = pool.getUsage();
if (usage != null && usage.getUsed() > 0) {
double poolRatio = (double) usage.getUsed() / usage.getMax();
if (poolRatio > 0.85) {
System.err.println("WARNING: Pool " + pool.getName() + " above 85%!");
}
}
}
}
}
Creating a Custom MBean
import javax.management.*;
import java.lang.management.*;
public class CacheMetricsMBean implements DynamicMBean {
private final CacheStats stats = new CacheStats();
@Override
public MBeanInfo getMBeanInfo() {
try {
MBeanAttributeInfo[] attributes = {
new MBeanAttributeInfo("HitCount", long.class.getName(), "Cache hits", true, false, false),
new MBeanAttributeInfo("MissCount", long.class.getName(), "Cache misses", true, false, false),
new MBeanAttributeInfo("HitRate", double.class.getName(), "Cache hit rate", true, false, false),
new MBeanAttributeInfo("Size", int.class.getName(), "Current cache size", true, false, false),
};
MBeanOperationInfo[] operations = {
new MBeanOperationInfo("reset", "Reset statistics", null, "void", MBeanOperationInfo.ACTION),
new MBeanOperationInfo("evict", "Evict oldest entries", new MBeanParameterInfo[]{new MBeanParameterInfo("count", int.class.getName(), "Number to evict")}, "void", MBeanOperationInfo.ACTION),
};
return new MBeanInfo(this.getClass().getName(), "Cache statistics", attributes, null, operations, null);
} catch (JMException e) {
throw new RuntimeException(e);
}
}
@Override
public Object getAttribute(String attribute) throws AttributeNotFoundException {
return switch (attribute) {
case "HitCount" -> stats.hitCount;
case "MissCount" -> stats.missCount;
case "HitRate" -> stats.getHitRate();
case "Size" -> stats.size;
default -> throw new AttributeNotFoundException(attribute);
};
}
@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException {
throw new AttributeNotFoundException("Read-only attributes");
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature) {
return switch (actionName) {
case "reset" -> { stats.reset(); yield null; }
case "evict" -> { stats.evict((Integer) params[0]); yield null; }
default -> throw new UnsupportedOperationException(actionName);
};
}
static class CacheStats {
long hitCount = 0;
long missCount = 0;
int size = 0;
double getHitRate() {
long total = hitCount + missCount;
return total == 0 ? 0.0 : (double) hitCount / total;
}
void reset() { hitCount = 0; missCount = 0; }
void evict(int count) { size = Math.max(0, size - count); }
}
}
Registering a Custom MBean
import javax.management.*;
public class MBeanRegistration {
public void registerCacheMetrics() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
CacheMetricsMBean cacheMetrics = new CacheMetricsMBean();
ObjectName name = new ObjectName("com.myapp:type=Cache,name=RequestCache");
// Register the MBean
mbs.registerMBean(cacheMetrics, name);
System.out.println("Registered: " + name);
}
public void unregisterMBean(String objectName) throws MalformedObjectNameException, InstanceNotFoundException, MBeanRegistrationException {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName(objectName);
mbs.unregisterMBean(name);
}
}
Using @MXBean Annotation (Simpler Approach)
import javax.management.mxbean.*;
@MXBean
public interface HttpRequestMetricsMXBean {
int getActiveRequests();
long getTotalRequests();
double getAverageLatencyMs();
void reset();
}
public class HttpRequestMetrics implements HttpRequestMetricsMXBean {
private final AtomicInteger active = new AtomicInteger();
private final AtomicLong total = new AtomicLong();
private final DoubleAdder latency = new DoubleAdder();
@Override
public int getActiveRequests() { return active.get(); }
@Override
public long getTotalRequests() { return total.get(); }
@Override
public double getAverageLatencyMs() {
long t = total.get();
return t == 0 ? 0.0 : latency.sum() / t;
}
@Override
public void reset() {
active.set(0);
total.set(0);
latency.reset();
}
public void recordRequest(long latencyMs) {
total.incrementAndGet();
latency.add(latencyMs);
}
public void requestStarted() { active.incrementAndGet(); }
public void requestCompleted() { active.decrementAndGet(); }
}
// Registration:
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
mbs.registerMBean(new HttpRequestMetrics(), new ObjectName("com.myapp:type=HttpRequests"));
Connecting via JMX Remote
import javax.management.remote.*;
public class JmxRemoteClient {
public void connect(String host, int port) throws Exception {
JMXServiceURL url = new JMXServiceURL(
"service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi"
);
JMXConnector connector = JMXConnectorFactory.connect(url);
MBeanServerConnection connection = connector.getMBeanServerConnection();
// Read MemoryMXBean attribute
ObjectName memoryName = new ObjectName("java.lang:type=Memory");
MemoryUsage heap = (MemoryUsage) connection.getAttribute(memoryName, "HeapMemoryUsage");
System.out.println("Heap used: " + heap.getUsed());
connector.close();
}
}
To enable remote JMX access, start the JVM with these flags:
java -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9999 \
-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.access.file=/path/to/jmxremote.access \
-Dcom.sun.management.jmxremote.password.file=/path/to/jmxremote.password \
-Djava.rmi.server.hostname=your.host.com \
myapp.jar
Production Failure Scenarios
Scenario 1: Memory Leak Detected via MemoryPoolMXBean
Symptom: Old generation pool grows continuously, GC reclaim less and less over time.
JMX Investigation:
List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean pool : pools) {
MemoryUsage usage = pool.getUsage();
if ("Old Gen".equals(pool.getName()) && usage != null) {
System.out.println("Old Gen: " +
String.format("%.2f%% used", 100.0 * usage.getUsed() / usage.getMax()));
}
}
What JMX showed: The Old Gen pool grew from 2GB to 14GB over 3 days without ever stabilizing. GC was running constantly but reclaiming almost nothing.
Root Cause: Someone put a ConcurrentHashMap as an in-memory session cache and never cleaned it out.
Scenario 2: Thread Deadlock Detected via ThreadMXBean
Symptom: Application freezes, no new requests processed, but process is not OOM or CPU-maxed.
JMX Investigation:
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlocks = threadMXBean.findDeadlockedThreads();
if (deadlocks != null) {
System.out.println("Deadlock detected: " + deadlocks.length + " threads");
for (long id : deadlocks) {
ThreadInfo info = threadMXBean.getThreadInfo(id);
System.out.println(" " + info.getThreadName() + " waiting on " + info.getLockName());
}
}
What JMX showed: Two thread pools were deadlocked on each other—one holding a database connection the other wanted, and vice versa. Classic circular wait.
Scenario 3: JIT Compilation Causing Latency Spikes
Symptom: Periodic latency spikes correlated with CompilationMXBean activity.
JMX Investigation:
CompilationMXBean compilationMXBean = ManagementFactory.getCompilationMXBean();
System.out.println("Compiler: " + compilationMXBean.getName());
System.out.println("Total compile time: " + compilationMXBean.getCompilerTotalTime() + " ms");
// Correlate with OSMXBean for system load
OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
System.out.println("System load: " + osMXBean.getSystemLoadAverage());
What JMX showed: The JVM was spending 30% of CPU time JIT-compiling hot methods right in the middle of peak traffic. P99 went from 5ms to 800ms like clockwork.
Trade-off Table
| Aspect | JMX Direct | JMX over RMI | JMX Exporter (Prometheus) |
|---|---|---|---|
| Latency overhead | <1ms | 5-50ms network | Scraped every 15s |
| Scalability | Single client only | Multiple clients | Unlimited (pull model) |
| Authentication | File-based | File-based | Depends on exporter |
| Firewall friendly | No | Yes (port 9999) | Yes (HTTP) |
| Production overhead | Low-Medium | Medium | Very Low |
| Metric retention | Real-time only | Real-time only | Long-term storage |
| TLS support | Weak | Supported | Full |
Observability Checklist
- Enable remote JMX with authentication and SSL in production
- Register
MemoryMXBeanpolling in your monitoring system - Monitor
GarbageCollectorMXBeanCollectionTime and CollectionCount per pool - Use
ThreadMXBean.findDeadlockedThreads()in health checks - Set up alerting on
HeapMemoryUsage.used / HeapMemoryUsage.max > 0.85 - Use
OperatingSystemMXBean.getSystemLoadAverage()for capacity planning - Expose custom MBeans for business metrics (queue depths, cache stats)
- Use
@MXBeanannotation for new MBeans (simpler thanDynamicMBean) - Document all custom MBean ObjectNames in a registry
- Rotate JMX passwords and restrict access via network policy
- Consider JMX Exporter instead of direct RMI for Prometheus/Grafana integration
- Monitor
CompilationMXBean.getCompilerTotalTime()to detect JIT issues
Security Notes
JMX exposes significant control surface area. Treat it accordingly.
Risks:
- Unauthenticated JMX allows arbitrary code execution on the JVM
ThreadMXBeanandRuntimeMXBeanexpose internal architecture details- Operations like
System.gc()can be invoked remotely - Connection credentials stored in plain text files
Hardening Steps:
# Require authentication
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.password.file=/path/to/jmxremote.password
# Use SSL for RMI
-Dcom.sun.management.jmxremote.ssl=true
-Djavax.net.ssl.keyStore=/path/to/keystore
-Djavax.net.ssl.keyStorePassword=password
# Restrict to localhost if not needed remotely
-Dcom.sun.management.jmxremote.host=127.0.0.1
# Disable dangerous operations
-Dcom.sun.management.jmxremote.disableCallerPrincipalCheck=false
Network Controls:
- Never expose JMX ports directly to the internet
- Use VPN or bastion host for JMX access
- Consider a JMX-to-HTTP bridge (like Jolokia) for safer remote access
- Firewall JMX ports to specific management IPs only
Common Pitfalls / Anti-Patterns
Pitfall 1: Forgetting to Unregister MBeans on Shutdown
MBeans registered but never unregistered will linger in the MBean server after redeployment. This causes InstanceAlreadyExistsException when you try to redeploy, or worse, memory leaks if something still holds references.
Implement MBeanRegistration or use shutdown hooks to unregister.
Pitfall 2: Blocking in MBean Attribute Getters
A getter that does any real work—database calls, locks, computation—blocks the MBean server thread and stalls every other client hitting the server. Keep getters fast. Do heavy work in background threads and cache results.
Pitfall 3: JMX RMI Port Conflicts
java.rmi.server.hostname defaults to the JVM’s internal view of its hostname, which is often wrong for networked deployments. RMI callbacks then bind to the wrong interface, and remote clients connect but never receive responses.
Always set -Djava.rmi.server.hostname=<public hostname> explicitly.
Pitfall 4: SSL Configuration Mismatch
JMX over SSL fails in confusing ways when client and server disagree on TLS versions or when client certificates are required but not provided. The error messages are not helpful.
Test SSL JMX in staging before relying on it in production.
Pitfall 5: MBean ObjectName Collisions
Two components registering MBeans with the same ObjectName causes one registration to fail silently (or throw InstanceAlreadyExistsException). Use a registry and follow reverse-domain naming: com.company:type=Component,name=Instance.
Quick Recap Checklist
- JMX and MXBeans expose JVM and application metrics via a standard API
- Built-in MXBeans cover memory, GC, threads, class loading, and compilation
- Use
ManagementFactory.getPlatformMBeanServer()to access the MBean server - Create custom MBeans with
@MXBeanannotation for simpler implementation - Register MBeans with unique ObjectNames following reverse domain notation
- Enable authentication, SSL, and network restrictions for remote JMX
- Consider JMX Exporter for Prometheus/Grafana instead of direct RMI
- Monitor deadlocks via
ThreadMXBean.findDeadlockedThreads() - Alert on heap usage > 85% and rising GC times
- Always unregister MBeans on application shutdown to avoid leaks
Interview Questions
MBeans are the general-purpose managed beans in JMX that require explicit interface implementation and manual attribute/operation definition. MXBeans (Managed Extension Beans) are a specific type of MBean that uses standard naming conventions and maps complex types through an MXBean proxy, making them easier to work with. The key difference is that MXBeans automatically handle type mapping for common types like MemoryUsage and ThreadInfo across JVM implementations, while regular MBeans require you to define everything explicitly. Platform MXBeans like MemoryMXBean and ThreadMXBean are all MXBeans.
Monitor MemoryPoolMXBean for each memory pool over time. Specifically, look at the old generation or tenured space: if getUsage().getUsed() grows steadily without stabilization even after GC runs, you have a leak. Log the CollectionCount and CollectionTime from GarbageCollectorMXBean — if GC runs frequently but reclaims little memory, objects are being retained. Alert when heap usage exceeds 85% or when GC time increases significantly without corresponding memory reduction.
Use ThreadMXBean.findDeadlockedThreads() which returns an array of thread IDs that are currently deadlocked. Call this periodically in a background thread and log the results. To get details, pass those IDs to getThreadInfo(long[] ids) which gives you thread names, state, and lock info. You can also use findMonitorDeadlockedThreads() for pure monitor-based deadlocks. Implement this in a health check endpoint so your monitoring system can alert when deadlocks appear.
Unauthenticated JMX allows complete JVM control — an attacker can invoke System.gc(), dump the heap, or execute arbitrary code through custom MBean operations. Beyond that, ThreadMXBean and RuntimeMXBean reveal internal architecture, and the InputArguments attribute exposes all JVM flags including potential secrets passed as system properties. The RMI protocol has had remote code execution vulnerabilities. Never expose JMX without authentication, SSL, and network-level restrictions. Use a VPN or bastion host for access, and consider Jolokia (HTTP-to-JMX bridge) for safer remote access patterns.
The MBeanServer is the agent layer in JMX — a registry that holds all registered MBeans and routes incoming requests (getAttribute, setAttribute, invoke) to the correct MBean based on its ObjectName. When you call mbs.getAttribute(name, "ThreadCount"), the MBeanServer looks up the MBean registered under that ObjectName, finds the corresponding attribute descriptor, and calls the MBean's getAttribute method. The MBeanServer also handles security (permission checks), concurrency (calls are serialized per MBean), and lifecycle (notifying listeners on registration/unregistration). There is one MBeanServer per JVM, accessible via ManagementFactory.getPlatformMBeanServer().
With @MXBean, you define a simple Java interface where method names following the getXxx, setXxx, and isXxx conventions are automatically exposed as attributes and operations. The interface implementation does not need to implement any JMX interfaces. The MXBean framework automatically handles type mapping for standard types (String, Integer, Long, etc.) and maps complex types through a standardized type registry so they work across different JVM implementations. With DynamicMBean, you must manually construct MBeanAttributeInfo, MBeanOperationInfo, and implement getAttribute/setAttribute/invoke yourself — which is error-prone and verbose.
MemoryMXBean gives you an overall view of JVM memory — heap and non-heap (metaspace, code cache, compressed class space) totals. It is useful for high-level memory monitoring. MemoryPoolMXBean gives you per-pool granularity — each pool (Eden, Survivor, Old Gen, Metaspace, Compressed Class Space, Code Cache) has its own MXBean. You get usage statistics for each pool individually, which is essential for understanding which generation is filling up during a memory leak. Use MemoryPoolMXBeans to track old generation growth for heap leaks, or metaspace growth for classloader leaks.
GarbageCollectorMXBean exposes CollectionCount (total number of GC cycles) and CollectionTime (total time spent in GC). Track both together to compute average GC time per collection. If CollectionTime is growing faster than CollectionCount, GC is spending more time per collection (typically old gen GC). Correlate with memory pool usage: if old gen is near capacity and GC time is rising, you have a retention issue. If young gen collections are frequent with short times but old gen keeps growing, objects are aging into old gen prematurely.
Unauthenticated JMX remote access is a critical vulnerability. An attacker who can reach the JMX port can: invoke System.gc() to trigger denial of service, dump the entire heap to inspect application data, invoke arbitrary MBean operations including custom ones that may execute code or modify application state, read RuntimeMXBean.getInputArguments() which exposes all JVM flags and potentially secrets passed as system properties. There are historical RCE vulnerabilities in JMX remote that allowed remote code execution without authentication. Always enable password authentication, SSL, and restrict network access.
Implement javax.management.NotificationListener and register it with the MBeanServer for specific MBeans. For example, register a listener on the MemoryMXBean to receive notifications when heap usage exceeds a threshold, or on a GarbageCollectorMXBean to alert when GC time spikes. Use mbs.addNotificationListener(objectName, listener, filter, handback). Create a notification filter to only receive notifications matching specific criteria. In production, push these notifications to your alerting system (PagerDuty, Slack) rather than relying on someone watching a JMX console.
OperatingSystemMXBean is a best-effort interface — the attributes it exposes vary by OS. On Linux, it includes getSystemLoadAverage(), getAvailableProcessors(), and physical memory stats. On Windows, you get similar data but accessed differently. Some attributes like getProcessCpuTime() are available on all platforms. Attributes marked as "unsupported" throw an IllegalArgumentException when accessed on an unsupported platform. For cross-platform compatibility, always guard attribute access with try-catch.
JMX and JFR are complementary and can absolutely be used together. JMX gives you real-time numeric snapshots (current heap used, current thread count) at the precision of your polling interval. JFR gives you event sequences and historical context. Use JMX for real-time alerting thresholds (e.g., alert when heap > 85%) and use JFR for root-cause investigation when those alerts fire. The FlightRecorderMXBean is itself a JMX MXBean — you control JFR from JMX. You can use JMX's ThreadMXBean for deadlock detection and JFR's Deoptimization events for deeper JIT diagnostics simultaneously.
You cannot register the same MBean instance under multiple ObjectNames directly. However, you can create a delegating MBean that implements DynamicMBean and delegates getAttribute/invoke calls to the same underlying implementation object for different ObjectName patterns. Some teams also use the JMX standard domain to register multiple names under the same domain with different key properties, or create a wrapper MBean that forwards to the same underlying implementation.
Most MXBean attribute reads are cheap — they return values cached in the JVM. However, some attributes trigger safepoints: MemoryPoolMXBean.getUsage() for some pools and ThreadMXBean.findDeadlockedThreads() can be expensive on busy JVMs. Minimize overhead by: polling only attributes you need, using longer polling intervals for stable metrics (30s for memory, 60s for thread count), caching results and publishing deltas rather than raw values, and avoiding findDeadlockedThreads() on every poll.
ClassLoadingMXBean exposes three metrics: LoadedClassCount (current classes in memory), TotalLoadedClassCount (cumulative classes loaded since JVM start), and UnloadedClassCount (cumulative classes unloaded). Rapidly increasing LoadedClassCount suggests dynamic class generation (common in ORM frameworks, scripting engines, JSP containers). A growing UnloadedClassCount with stable LoadedClassCount is normal in long-running servers. Unexpected class loading spikes can indicate classloader leaks or memory bloat from excessive dynamic proxy generation.
JMX over RMI uses two ports: a registry port (default 9999) where the RMI registry runs, and an anonymous export port where the actual MBeanServer communication happens. The JVM's java.rmi.server.hostname setting determines what address the client is told to connect to for callbacks. Failure modes: wrong hostname causes clients to connect to the registry but fail to receive callback data, firewall blocking the anonymous port causes connection to appear to establish but hang, and serialization of nonSerializable objects across the RMI boundary crashes connections.
A StandardMBean implements a management interface you define (either explicitly or via the @MXBean annotation) and the MBeanServer automatically exposes methods matching standard naming conventions (getXxx, setXxx, isXxx for attributes; other methods as operations). A DynamicMBean implements DynamicMBean and you manually construct MBeanInfo, MBeanAttributeInfo, and MBeanOperationInfo objects at runtime. DynamicMBean gives you full flexibility to build interfaces dynamically based on configuration. StandardMBean (and @MXBean) is preferred when the interface is known at compile time because it is simpler and less error-prone.
The MBeanServer serializes requests per MBean — concurrent requests for different MBeans are handled concurrently, but requests to the same MBean are queued. This means a slow attribute getter on one MBean blocks other requests to that same MBean but not to other MBeans. For remote access, each RMI connection has its own thread, but the MBeanServer layer introduces serialization. Design MBean getters to be fast (return cached values, do no I/O or locking). If you need concurrent execution, expose an operation that returns a Future and does the heavy work in a background thread.
CompilationMXBean exposes getName() (compiler name, e.g., HotSpot or OpenJ9), getTotalCompilationTime() (cumulative CPU time spent compiling), and in newer JVMs, per-compiler statistics. A rapidly increasing TotalCompilationTime during steady-state operation suggests the JVM is spending significant CPU on JIT compilation — which can cause latency spikes if it triggers deoptimization. Compare TotalCompilationTime growth rate against your application uptime to estimate compilation overhead.
The MBeanServer uses ObjectName pattern matching with wildcards for querying and setting up notifications. A query like com.myapp:type=*,name=Cache matches all MBeans in the com.myapp domain with any type but a specific name property of Cache. Notifications work similarly — registering a notification listener with a wildcard pattern means you receive notifications from all matching MBeans. This is useful for centralized monitoring where one listener handles events from multiple MBeans. The pattern syntax follows the ObjectName conventions where * matches any value and commas separate key properties.
Further Reading
- MXBean Names and Types - Platform MXBean reference
- Jolokia - JMX over HTTP - REST-style JMX access bridge
- Prometheus JMX Exporter - Convert JMX to Prometheus metrics
- JMX Remote API - RMI and JMX connector documentation
Conclusion
JMX and MXBeans provide the standard Java platform mechanism for JVM and application monitoring. The built-in platform MXBeans cover memory, GC, threads, class loading, and compilation. For custom metrics, use the @MXBean annotation to create simpler MBeans. Always enable authentication and SSL for remote JMX access, and consider JMX Exporter for Prometheus integration rather than direct RMI connections.
Category
Related Posts
Java Flight Recorder: Continuous Monitoring and Diagnostics
Learn how Java Flight Recorder captures low-level diagnostics, profiling data, and continuous monitoring events from the JVM in production environments.
JVMTI Agents: Profiling and Debugging with the JVM Tool Interface
Explore the JVM Tool Interface for building profiling, debugging, and monitoring agents that hook deep into the JVM runtime.
Java Atomics and VarHandle: Low-Level Concurrency
Understanding Java atomic operations: AtomicInteger, AtomicReference, VarHandle, compareAndSet, atomics vs locks, and lock-free programming patterns.