CubeAPM
CubeAPM CubeAPM

JMX Monitoring: Setup Guide for Java Applications

JMX Monitoring: Setup Guide for Java Applications

Table of Contents

JMX (Java Management Extensions) gives you direct visibility into the JVM’s runtime behavior of memory usage, thread states, garbage collection activity, and custom application metrics. Without JMX, you are diagnosing production performance issues by parsing logs after the fact. With JMX enabled, you can query live metrics, identify memory leaks as they develop, and correlate GC pauses with user-facing latency spikes before they escalate into outages.

According to the 2025 CNCF Annual Survey, 78% of organizations now run containers in production, and Java applications remain among the most monitored workloads. Despite this, many teams overlook JMX configuration until a production incident forces them to add instrumentation retroactively.

This guide walks through enabling JMX on a Java application, configuring secure remote access, exposing custom MBeans, and troubleshooting the most common JMX setup failures. Every configuration example is tested on OpenJDK 11 and later.

Prerequisites

Before starting, ensure you have:

  • Java Development Kit (JDK) 8 or later installed
  • Terminal access to the server or container running your Java application
  • Firewall rules allowing the JMX port (default 9999) if monitoring remotely
  • Basic understanding of JVM startup parameters
  • A running Java application that you can restart with modified JVM arguments
  • If monitoring remotely: network connectivity between the monitoring client and the JVM host

Optional but recommended:

  • A JMX client like JConsole (bundled with the JDK) or VisualVM
  • SSH access if the JVM runs on a remote server
  • Docker installed if testing in containerized environments

Step 1: Enable JMX Agent with Local Access

The JMX agent is built into the JVM. You enable it by adding system properties when starting your Java application. Local access means the monitoring client runs on the same machine as the JVM, which is useful for development and initial testing.

Start your Java application with these JVM arguments:

java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=9999 \
     -Dcom.sun.management.jmxremote.authenticate=false \
     -Dcom.sun.management.jmxremote.ssl=false \
     -Dcom.sun.management.jmxremote.local.only=true \
     -jar your-application.jar

What each flag does:

  • com.sun.management.jmxremote — enables the JMX agent
  • com.sun.management.jmxremote.port=9999 — exposes JMX on port 9999
  • com.sun.management.jmxremote.authenticate=false — disables password authentication (acceptable for local-only testing, never for production)
  • com.sun.management.jmxremote.ssl=false — disables SSL (again, local testing only)
  • com.sun.management.jmxremote.local.only=true — restricts access to localhost

Once the application starts, verify JMX is active by connecting with JConsole:

jconsole localhost:9999

You should see live memory, threads, and GC metrics in the JConsole interface. If the connection fails, check that the port is not already in use and that no firewall is blocking localhost traffic.

This configuration is for local testing only — authentication and SSL are disabled. Never expose this setup to external networks.

Step 2: Configure Secure Remote JMX Access

For production monitoring, you need authentication, SSL, and restricted network access. Remote JMX without these controls exposes your JVM to unauthorized access and potential compromise.

Create a password file. This file defines JMX users and their passwords:

sudo mkdir -p /etc/jmx
sudo nano /etc/jmx/jmxremote.password

Add a user and password:

monitorUser  your_secure_password_here

Set restrictive file permissions so only the JVM process owner can read it:

sudo chmod 600 /etc/jmx/jmxremote.password
sudo chown jvmuser:jvmuser /etc/jmx/jmxremote.password

Replace jvmuser with the actual user running the JVM.

Create an access control file to define user permissions:

sudo nano /etc/jmx/jmxremote.access

Add this line:

monitorUser  readonly

This grants read only access. For full management access, use readwrite instead.

Set permissions:

sudo chmod 600 /etc/jmx/jmxremote.access
sudo chown jvmuser:jvmuser /etc/jmx/jmxremote.access

Start your application with these JVM arguments:

java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=9999 \
     -Dcom.sun.management.jmxremote.rmi.port=9999 \
     -Dcom.sun.management.jmxremote.authenticate=true \
     -Dcom.sun.management.jmxremote.password.file=/etc/jmx/jmxremote.password \
     -Dcom.sun.management.jmxremote.access.file=/etc/jmx/jmxremote.access \
     -Dcom.sun.management.jmxremote.ssl=false \
     -Djava.rmi.server.hostname=your-server-ip \
     -jar your-application.jar

Key additions:

  • com.sun.management.jmxremote.rmi.port=9999 — fixes the RMI port so you only need one firewall rule
  • java.rmi.server.hostname=your-server-ip — tells RMI to advertise the correct hostname or IP address for remote clients

Replace your-server-ip with the actual IP address or hostname that remote clients will use to connect.

Verify remote access from another machine:

jconsole your-server-ip:9999

Enter monitorUser and the password when prompted.

SSL is still disabled here. For production environments handling sensitive data, enable SSL by generating a keystore and adding -Dcom.sun.management.jmxremote.ssl=true along with keystore path and password properties. Full SSL setup is documented in Oracle’s JMX Remote SSL guide.

This setup estimates typical production access patterns used in secure monitoring environments. Actual network policies and compliance requirements vary — always verify current security requirements for your environment.

Step 3: Enable JMX in Containerized Java Applications

Docker containers add network isolation that complicates JMX remote access. The JVM inside the container advertises an RMI hostname that is unreachable from outside the container unless you explicitly configure it.

Modify your Dockerfile to expose the JMX port:

FROM openjdk:11-jre-slim
COPY target/your-application.jar /app/app.jar
EXPOSE 8080 9999
ENTRYPOINT ["java", \
            "-Dcom.sun.management.jmxremote", \
            "-Dcom.sun.management.jmxremote.port=9999", \
            "-Dcom.sun.management.jmxremote.rmi.port=9999", \
            "-Dcom.sun.management.jmxremote.authenticate=false", \
            "-Dcom.sun.management.jmxremote.ssl=false", \
            "-Djava.rmi.server.hostname=localhost", \
            "-jar", "/app/app.jar"]

Run the container with port mapping:

docker run -p 8080:8080 -p 9999:9999 your-image

Connect from your host machine:

jconsole localhost:9999

For Kubernetes deployments, use a Service with type NodePort or LoadBalancer to expose the JMX port. Alternatively, use kubectl port-forward for temporary access:

kubectl port-forward pod/your-pod-name 9999:9999

Then connect via jconsole localhost:9999.

In production Kubernetes environments, avoid exposing JMX ports externally. Instead, deploy a sidecar container running a JMX exporter that scrapes JMX metrics and exposes them in Prometheus format. This keeps JMX access internal while allowing tools that support infrastructure monitoring to collect metrics without direct JMX connections.

Step 4: Expose Custom Application Metrics via MBeans

JVM metrics like memory and threads are useful, but production troubleshooting often requires application specific metrics: active database connections, queue depths, cache hit rates, or business transaction counts.

You expose custom metrics by creating an MBean. Here is a simple example.

Define an MBean interface:

package com.example.monitoring;
public interface ApplicationMetricsMBean {
    int getActiveConnections();
    long getCacheHitRate();
    void resetCache();
}

Implement the interface:

package com.example.monitoring;
public class ApplicationMetrics implements ApplicationMetricsMBean {
    private int activeConnections = 0;
    private long cacheHits = 0;
    private long cacheMisses = 0;
    public void incrementConnections() {
        activeConnections++;
    }
    public void decrementConnections() {
        activeConnections--;
    }
    public void recordCacheHit() {
        cacheHits++;
    }
    public void recordCacheMiss() {
        cacheMisses++;
    }
    @Override
    public int getActiveConnections() {
        return activeConnections;
    }
    @Override
    public long getCacheHitRate() {
        long total = cacheHits + cacheMisses;
        return total == 0 ? 0 : (cacheHits * 100) / total;
    }
    @Override
    public void resetCache() {
        cacheHits = 0;
        cacheMisses = 0;
    }
}

Register the MBean when your application starts:

package com.example;
import com.example.monitoring.ApplicationMetrics;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
public class Application {
    public static void main(String[] args) throws Exception {
        ApplicationMetrics metrics = new ApplicationMetrics();
        
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        ObjectName name = new ObjectName("com.example:type=ApplicationMetrics");
        mbs.registerMBean(metrics, name);
        
        // Start your application
        // Use metrics.incrementConnections() wherever connections are opened
    }
}

Restart your application with JMX enabled. Open JConsole, navigate to the MBeans tab, and find com.exampleApplicationMetrics. You will see ActiveConnections and CacheHitRate attributes, and a resetCache operation you can invoke.

This pattern extends to any metric you want to track: HTTP request rates, error counts, processing latencies, or external API call durations.

Step 5: Connect a JMX Monitoring Tool

Once JMX is enabled, you need a tool to collect and visualize metrics continuously. JConsole is useful for live debugging but is not designed for production monitoring.

Production options include:

Prometheus with JMX Exporter: Deploy the JMX Exporter as a Java agent. It scrapes JMX metrics and exposes them in Prometheus format. Prometheus then scrapes the exporter endpoint.

Example JVM arguments:

java -javaagent:jmx_prometheus_javaagent-0.17.2.jar=8080:config.yaml \
     -jar your-application.jar

Prometheus scrapes http://your-host:8080/metrics and stores time series data. Grafana dashboards visualize the metrics.

Datadog Agent: The Datadog Java integration uses JMX to collect metrics and forwards them to Datadog’s platform. Configuration is documented in Datadog’s Java integration guide.

New Relic Java Agent: New Relic’s APM agent instruments Java applications and includes JMX metric collection. It is agent-based rather than using remote JMX connections.

CubeAPM: For teams running container monitoring tools alongside APM, CubeAPM connects to JMX endpoints via OpenTelemetry or Prometheus exporters. It provides unified visibility into JVM metrics, application traces, and logs without requiring separate tools for each signal type. CubeAPM runs on your infrastructure, so JMX telemetry never leaves your cloud environment. This matters for regulated industries and teams with data residency requirements. Pricing is $0.15/GB for all ingested telemetry with no per-host or per-user fees.

For local development and debugging, JConsole and VisualVM remain the fastest options. For production, use a monitoring platform that retains metrics over time and correlates JMX data with application traces and logs.

Step 6: Automate JMX Configuration in Deployment Pipelines

Manually adding JVM arguments works for testing but does not scale across environments. Most teams externalize JMX configuration using environment variables or configuration management tools.

Example using environment variables in a systemd service file:

[Unit]
Description=Java Application
After=network.target
[Service]
Type=simple
User=jvmuser
Environment="JAVA_OPTS=-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9999 \
-Dcom.sun.management.jmxremote.rmi.port=9999 \
-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.password.file=/etc/jmx/jmxremote.password \
-Dcom.sun.management.jmxremote.access.file=/etc/jmx/jmxremote.access \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=192.168.1.10"
ExecStart=/usr/bin/java $JAVA_OPTS -jar /opt/app/application.jar
Restart=on-failure
[Install]
WantedBy=multi-user.target

Reload systemd and start the service:

sudo systemctl daemon-reload
sudo systemctl start your-app
sudo systemctl enable your-app

For Kubernetes, define JMX arguments in a ConfigMap and reference them in the pod spec:

apiVersion: v1
kind: ConfigMap
metadata:
  name: jmx-config
data:
  JAVA_OPTS: >
    -Dcom.sun.management.jmxremote
    -Dcom.sun.management.jmxremote.port=9999
    -Dcom.sun.management.jmxremote.rmi.port=9999
    -Dcom.sun.management.jmxremote.authenticate=false
    -Dcom.sun.management.jmxremote.ssl=false
---
apiVersion: v1
kind: Pod
metadata:
  name: java-app
spec:
  containers:
  - name: app
    image: your-image
    env:
    - name: JAVA_OPTS
      valueFrom:
        configMapKeyRef:
          name: jmx-config
          key: JAVA_OPTS
    ports:
    - containerPort: 9999

This approach keeps JMX configuration version-controlled and consistent across environments.

Troubleshooting Common Issues

Connection refused when connecting remotely

Symptom: JConsole or another client cannot connect to the JMX port even though the application started without errors.

Causes:

  • Firewall blocking the JMX port
  • java.rmi.server.hostname not set or set incorrectly
  • RMI registry port differs from the JMX port

Solution:

Verify the port is open:

netstat -tuln | grep 9999

If nothing appears, the JMX agent did not start. Check JVM arguments for typos.

If the port is open but connections fail, ensure java.rmi.server.hostname is set to the IP address or hostname clients use to reach the server. If you are connecting via SSH tunnel, set it to localhost.

Test connectivity with telnet:

telnet your-server-ip 9999

If telnet fails, the issue is network level. Check firewall rules and security groups.

Authentication failure with “Invalid username or password”

Symptom: JConsole prompts for credentials but rejects them.

Causes:

  • Password file permissions too open (must be 600)
  • Incorrect username or password in the file
  • Password file path wrong in JVM arguments

Solution:

Verify file permissions:

ls -l /etc/jmx/jmxremote.password

Should show -rw------- and be owned by the JVM user.

Check the password file contents. Ensure no extra spaces or hidden characters. The format is strict:

username password

Restart the JVM after correcting the file.

High CPU usage after enabling JMX

Symptom: CPU usage increases noticeably after enabling JMX, even with no clients connected.

Cause: If you enabled JMX with very frequent metric polling or registered many MBeans with expensive attribute calculations, the overhead can become measurable.

Solution:

Profile which MBeans are being queried frequently. Use JConsole or VisualVM to see which attributes are slow to compute.

Avoid expensive operations in MBean getter methods. Cache computed values and refresh them on a background thread rather than recalculating on every JMX query.

Reduce the polling frequency in your monitoring tool. Polling every second is rarely necessary for most metrics. Every 15 to 60 seconds is typical.

JMX Exporter shows no metrics

Symptom: Prometheus JMX Exporter is running but http://localhost:8080/metrics returns an empty response or only JVM default metrics.

Cause: The exporter configuration file does not match the MBean object names in your application.

Solution:

Check the exporter logs for connection errors:

java -javaagent:jmx_prometheus_javaagent.jar=8080:config.yaml -jar app.jar

Verify the MBean names using JConsole. Compare them to the patterns in config.yaml.

Example config snippet:

rules:
- pattern: "com.example<type=ApplicationMetrics><>(.*)"
  name: application_$1
  type: GAUGE

Adjust the pattern to match your actual MBean object names.

JMX connection works locally but not from Docker host

Symptom: JConsole connects to localhost:9999 inside the container but not from the Docker host machine.

Cause: java.rmi.server.hostname is set to localhost, which resolves to the container’s loopback interface, not the host.

Solution:

Set java.rmi.server.hostname to 0.0.0.0 if you want the JMX server to bind to all interfaces:

-Djava.rmi.server.hostname=0.0.0.0

Alternatively, set it to the container’s IP address or use host networking mode:

docker run --network host your-image

Host networking removes container network isolation, so the JVM binds directly to the host’s network stack.

Frequently Asked Questions

What is the difference between JMX and APM tools?

JMX is a Java standard for exposing runtime metrics from the JVM. APM tools consume JMX data along with traces, logs, and infrastructure metrics to provide full application observability. JMX gives you raw data; APM tools structure and correlate it for production monitoring and troubleshooting.

Can I use JMX in production without authentication?

No. Disabling authentication exposes management operations to anyone who can reach the JMX port. An attacker could invoke operations to trigger garbage collection, modify application state, or execute arbitrary code via MBeans. Always enable authentication and SSL in production environments.

How much overhead does JMX add?

Minimal when idle. The JMX agent itself consumes negligible CPU and memory. Overhead increases when clients actively query MBeans, especially if getter methods perform expensive calculations. Typical production monitoring with 60 second poll intervals adds less than 1% CPU overhead on modern JVMs.

What JMX port should I use?

9999 is conventional but not required. Choose any unused port above 1024. Ensure the same port is used for both `com.sun.management.jmxremote.port` and `com.sun.management.jmxremote.rmi.port` to simplify firewall rules.

How do I monitor multiple Java applications on the same server?

Assign each application a unique JMX port. Configure JVM arguments per application. For example, app1 uses port 9999, app2 uses 10000. Monitoring tools connect to each port separately. Alternatively, use JMX exporters for each application and let Prometheus scrape all of them.

Can I expose JMX metrics without opening a network port?

Yes, using the JMX Exporter as a Java agent. The exporter runs in-process, reads MBeans via the local MBean server, and exposes metrics on an HTTP endpoint. This avoids RMI remote connections entirely. Many teams prefer this approach because it integrates cleanly with Prometheus and eliminates RMI configuration complexity.

Does JMX work with non-Oracle JVMs?

Yes. JMX is part of the Java SE specification and is implemented by all major JVM vendors: Oracle HotSpot, OpenJDK, Amazon Corretto, Azul Zulu, Eclipse OpenJ9, and GraalVM. The JVM arguments and MBean APIs are standardized and portable across vendors.

Disclaimer: The information in this article reflects the latest details available at the time of publication and may change as technologies and products evolve. Features, pricing, and plan limits can change over time. Always verify the latest information directly with the vendor before making purchasing or deployment decisions.

×
×