|

Monitoring Java Applications with JMX


https://github.com/idylmz/JMXExampleProject

Years ago, when I joined a team working on resilience, we were asked to monitor a product developed in Java — without using an APM agent. Everyone on the team had the same question in mind:

“How are we going to see what’s going on inside this application without installing an agent?”

Since the product was developed by an external team, we consulted them about monitoring. Their answer was short and clear:

“You can connect via JMX.”

At the time, I neither knew Java nor had any idea what JMX was used for… We just ran the commands we were given, monitored what we needed to monitor, and the job was done.

But as time went on, I started to wonder how this mechanism actually worked behind the scenes.

So I decided to study JMX in detail, and I wanted to share the experience I gained in this article.

JMX is a powerful management and monitoring infrastructure that the Java world has been using for years.

It’s an API that allows you to monitor not only JVM metrics, but everything inside the application.

In this article, I will walk you through step by step:

  • How JMX works
  • How to write a custom MBean
  • How to expose JMX externally using RMI
  • And how to monitor everything live using JMC (Java Mission Control)

You can follow this article even without knowing any Java. For example, as a .NET developer, I learned just enough Java to write this article. 😄

Enjoy the article.


👀 What Is JMX?
 JMX (Java Management Extensions) is a built-in technology used to monitor and manage Java applications while they are running.

With JMX, from inside a Java application you can:

  • Read metrics. e.g., how many requests came in, how many operations completed, queue size, etc.
  • Change configuration settings. e.g., change the log level at runtime
  • Invoke operations. e.g., clear a cache, reset a service

In short:

JMX = a window that lets you control the inside of a live Java application.


🧱 Core Components of JMX

  • MBean => A manageable object
  • MBeanServer => The component that registers MBeans and exposes them externally
  • Connector => The way to access the MBeanServer
  • JMX Client => A tool that displays MBeans. This can be a GUI tool (like JConsole or Java Mission Control), or bridge tools that act as both clients and exporters to external systems (such as Prometheus JMX Exporter or the Logstash JMX plugin).

🫘 What Is an MBean? (No, not a literal bean 😄)
An MBean (Managed Bean) is the object in JMX that is monitored and managed.

With an MBean, you can:

✔ Read values
✔ Change values
✔ Invoke its methods (e.g., restart()clearCache())

For example:

public interface AppMetricsMBean {
long getRequestCount();
void incrementRequestCount();
}

🌍 How Do You Access JMX?

There are two scenarios:

I won’t spend much time on the first one, because it’s not really applicable in large-scale systems. If you want to access the MBeanServer of a local application, you can monitor it directly using a GUI tool. As I said, I won’t focus on this approach.

The second scenario looks like this: remote monitoring. This is where the concept of Remote Method Invocation (RMI) comes into play. JMX metrics exposed on a specific port (X) of the target server can be accessed using RMI. This is the main focus of this article.

Because, frankly, in large systems it’s neither realistic nor practical to connect to every single server one by one using Java Mission Control (JMC). In the end, we want to visualize these metrics, turn them into time series, and set up alerts. But regardless of which tool we use, our protocol will always be RMI.

service:jmx:rmi:///jndi/rmi://<host>:9999/jmxrmi

🎯 Why JMX Is Still Critical Today

Most modern observability tools are powered by JMX.

Prometheus, Grafana, Elastic APM, Dynatrace, and many more…

The reason something like Prometheus JMX Exporter exists is exactly this 😊

Additionally:

  • It offers a no-agent monitoring option
  • The best source to describe the JVM’s internals is the JVM itself
  • A remote control point in the CI/CD and container world

And all of this can be done with almost zero overhead, using Java’s own built-in mechanisms.

🧩 How to Create a JMX MBean

In the JMX world, manageable objects are called MBeans (Managed Beans).

To create an MBean, we actually need just two steps:

1️⃣ Define an MBean interface
2️⃣ Create a class that implements this interface

The only rule JMX enforces:

  • The interface name must be SomethingMBean
  • The class name must be Something — meaning the MBean suffix is removed

🔹 Step 1 — Create the Interface

The following interface will keep track of how many requests are made to the /hello endpoint:

public interface AppMetricsMBean {
long getRequestCount();
void incrementRequestCount();
}

Here:

  • getRequestCount() → Can be read as a value via JMX
  • incrementRequestCount() → Can be invoked as an operation via JMX
     (We’ll later click and see this manually in JMC.)

🔹 Step 2 — The Implementing Class

public class AppMetrics implements AppMetricsMBean {
private long requestCount = 0;
@Override
public synchronized long getRequestCount() {
return requestCount;
}
@Override
public synchronized void incrementRequestCount() {
requestCount++;
}
}

📝 Why synchronized?

Because multiple threads may access the MBean at the same time, and we want to preserve data consistency.

🎯 Where Are We at This Point?

✔ We wrote an MBean that holds our custom metric

❌ But we haven’t registered it with the JVM yet

📌 So right now, the MBean exists — but nobody knows about it.

For JMX to work, this MBean must be:

  • Registered with the JVM’s MBeanServer
  • Exposed to the outside world via RMI, if needed

That’s what we’ll do in the next section 👇

🔧 Registering the MBean in Spring Boot (MBeanServer Register)

The JMX MBeanServer is already running inside the JVM.

All we need to do is notify it of our custom MBean — in other words:

“I have a new object called AppMetrics that should be monitored!”

To do this automatically when the application starts, we’ll use a @Configuration class.

src/main/java/…/JmxConfig.java:

package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.rmi.registry.LocateRegistry;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
@Configuration
public class JmxConfig {
@Bean
public AppMetrics appMetricsMBean() throws Exception {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name =
new ObjectName("com.example.demo:type=ApplesAndOranges");
AppMetrics mbean = new AppMetrics();
if (!mbs.isRegistered(name)) {
mbs.registerMBean(mbean, name);
}
return mbean;
}
@Bean(initMethod = "start", destroyMethod = "stop")
public JmxRmiServer jmxRmiServer() {
return new JmxRmiServer(9999); // 9999: JMX RMI port
}
public static class JmxRmiServer {
private final int port;
private JMXConnectorServer connectorServer;
public JmxRmiServer(int port) {
this.port = port;
}
public void start() throws Exception {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
LocateRegistry.createRegistry(port);
String urlString = String.format(
"service:jmx:rmi:///jndi/rmi://0.0.0.0:%d/jmxrmi", port);
JMXServiceURL url = new JMXServiceURL(urlString);
connectorServer =
JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbs);
connectorServer.start();
System.out.println("JMX RMI connector started on: " + urlString);
}
public void stop() throws Exception {
if (connectorServer != null) {
connectorServer.stop();
}
}
}
}

At this point, we’ve defined our class as an MBean. As we’ll later see in JConsole, we’ll be able to read the counter value under the ApplesAndOranges label.

We’ve done everything — but we haven’t discussed how this value will be incremented. Yes, it can also be incremented externally. But in a realistic scenario, we want to read values calculated by the application itself.


📈 Updating an MBean from an HTTP Endpoint

Now let’s really “bring the MBean to life”.

That is, every time the /hello endpoint is called:

  • AppMetrics.incrementRequestCount() runs
  • The JMX metric value increases
  • And we observe it in JMC

1️⃣ CounterService

You can design CounterService clearly like this for this article:

package com.example.demo;
import org.springframework.stereotype.Service;
@Service
public class CounterService {
private final AppMetrics appMetrics;
public CounterService(AppMetrics appMetrics) {
this.appMetrics = appMetrics;
}
public void increment() {
appMetrics.incrementRequestCount();
}
}

2️⃣ HelloController

Let’s use this service every time a request hits /hello:

package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
private final CounterService counterService;
public HelloController(CounterService counterService) {
this.counterService = counterService;
}
@GetMapping("/hello")
public String hello() {
counterService.increment();
return "Hello from JMX!";
}
}

Now the flow looks like this:

  • A user sends a request to /hello
  • HelloController calls CounterService.increment()
  • CounterService calls AppMetrics.incrementRequestCount()
  • requestCount inside AppMetrics increases
  • This value becomes readable externally as a JMX MBean attribute

👀 Live Monitoring via JMC

Now you have:

  • A running Spring Boot application
  • /hello endpoint
  • An MBean exposed via JMX + RMI

Let’s visualize it.

Start the application:

./mvnw spring-boot:run

Run JConsole:

jconsole
JConsole Screen

At this point, it’s worth mentioning something. We actually registered only one custom metric. However, the MBeanServer already contains metrics related to the JVM itself — such as CPU usage, memory consumption, and other basic infrastructure metrics. If your application registers business-critical metrics, you can monitor those as well.

Default JVM Metrics Being Collected

To view the metrics registered as MBeans, switch to the MBeans tab.

As you can see, our RequestCount metric is now visible. Since there have been no requests yet, its value is 0. Let’s send a request:

curl http://localhost:8080/hello

As you can see, with the incoming request, the counter increased by one. But earlier in the article, we also mentioned that we could trigger things directly from JConsole — and we even wrote a method for that. What happens to it?

Now we’ll see this method under Operations.

From here, we can trigger the method we exposed. When we check the counter again, we’ll see that the value has increased. Of course, we won’t use this to “cheat” request counts — but for important operations like clearing caches 😊

I accidentally triggered it twice while taking screenshots. The value became 3.

In this article, we laid the foundations of JMX:

  • We created a custom MBean
  • Exposed it via RMI
  • And monitored it live using JMC

In the next article, we’ll connect these metrics to a modern observability pipeline using Prometheus and Grafana.

Stay tuned! 🚀

Similar Posts

  • |

    Master Your Alerts with Prometheus and Alertmanager

    To ensure that applications and services run smoothly, collecting metrics alone is not sufficient. What really matters is being notified when something unusual happens in those metrics. This is where the Prometheus + Alertmanager duo comes into play. In this article, I will focus more on the alerts that can be defined directly in Prometheus….

  • |

    What is Prometheus and How to Add Prometheus To Your .NET Projects?

    Requirements: Latest versions of Docker and Visual Studio Hello all, Monitoring is very essential in todays fast pacing development environments. We can divide monitoring into different aspects such as tracing, collecting metrics and visualizing metrics. For tracing, we have covered OpenTelemetry and Zipkin here. Today we will talk about collecting any metrics within our application. There…

Leave a Reply

Your email address will not be published. Required fields are marked *