12 Jmx and Related Tools High Mountains and Small Moons, Waterfalls Revealing Stones

The Java platform provides comprehensive JVM monitoring and management measures.

Before Java SE 5, although the JVM provided some low-level APIs such as JVMPI and JVMTI, these APIs were targeted at the C language and required JNI calls, making it inconvenient to monitor JVM and system resources.

Java SE 5.0 introduced JMX technology (Java Management Extensions), which was preceded by “JSR3: Java Management Extensions” and “JSR 160: JMX Remote API”.

JMX is a set of standard APIs used to monitor and manage JVM resources, including applications, devices, services, and the JVM itself.

Through these API interfaces, some information about the JVM and the host machine can be exposed, and even support remote dynamic adjustment of certain runtime parameters.

JMX technology makes it possible to develop self-checking programs in the JDK, and also provides many lightweight APIs to monitor JVM status and running object/thread status, thereby improving the management and monitoring capabilities of the Java language itself.

Clients use JMX primarily in two ways:

  • Manually obtaining MXBeans through program code;
  • Obtaining MBeans remotely through the network.

The simplest way to obtain GC behavior data from the JVM at runtime is to use the standard JMX API interface. JMX is also the standard API for obtaining internal runtime information of the JVM. Program code can be written to access the JVM where the program is running through the JMX API, or remote access can be performed through JMX clients. MXBeans can be used to monitor and manage the JVM, with each MXBean encapsulating a part of the functionality.

In simpler terms, we can establish a “government information disclosure system” within the JVM. This system can be seen as the MBeanServer, and the default information of the system, such as basic information about the JVM, memory, and GC, can be made public through this system. Other Java code in the application can also define their own MBeans and hang these self-desired information onto this system. At this point, whether it is within the JVM or other Java applications, all the public information, i.e. the properties of the MBeans, can be accessed, and the methods provided by the MBeans can even be directly called to influence the system in return.

Obtaining MXBean Information for the Current JVM #

The MXBean-related classes provided by the JDK are mainly located in the java.lang.management package in the rt.jar file. The following code example shows how to obtain MXBean information from the JVM:

package jvm.chapter11;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.*;
import java.lang.management.*;
import java.util.*;

public class MXBeanTest {
    public static void main(String[] args) {
        Map<String, Object> beansMap = loadMXBeanMap();
        String jsonString = toJSON(beansMap);
        System.out.println(jsonString);
    }
    public static Map<String, Object> loadMXBeanMap() {
        // import java.lang.management.*
        // 1. Operating system information
        OperatingSystemMXBean operatingSystemMXBean =
                ManagementFactory.getOperatingSystemMXBean();
        // 2. Runtime
        RuntimeMXBean runtimeMXBean =
                ManagementFactory.getRuntimeMXBean();
        // 3.1 JVM memory information
        MemoryMXBean memoryMXBean =
                ManagementFactory.getMemoryMXBean();
        // 3.2 JVM memory pool - list
        List<MemoryPoolMXBean> memoryPoolMXBeans =
                ManagementFactory.getMemoryPoolMXBeans();
        // 3.3 Memory manager - list
        List<MemoryManagerMXBean> memoryManagerMXBeans =
                ManagementFactory.getMemoryManagerMXBeans();
        // 4. Class loading statistics
        ClassLoadingMXBean classLoadingMXBean =
                ManagementFactory.getClassLoadingMXBean();
        // 5. Compilation statistics
        CompilationMXBean compilationMXBean =
                ManagementFactory.getCompilationMXBean();
        // 6. Thread
        ThreadMXBean threadMXBean =
                ManagementFactory.getThreadMXBean();
        // 7. GC
        List<GarbageCollectorMXBean> garbageCollectorMXBeans =
                ManagementFactory.getGarbageCollectorMXBeans();
        // 8. Get the platform logging MXBean
        PlatformLoggingMXBean platformLoggingMXBean =
                ManagementFactory.getPlatformMXBean(PlatformLoggingMXBean.class);
        //
        Map<String, Object> beansMap = new HashMap<String, Object>();
        //
        beansMap.put("operatingSystemMXBean", operatingSystemMXBean);
        beansMap.put("runtimeMXBean", runtimeMXBean);
        beansMap.put("memoryMXBean", memoryMXBean);
        beansMap.put("memoryPoolMXBeans", memoryPoolMXBeans);
        beansMap.put("memoryManagerMXBeans", memoryManagerMXBeans);
        beansMap.put("classLoadingMXBean", classLoadingMXBean);
        beansMap.put("compilationMXBean", compilationMXBean);
        beansMap.put("threadMXBean", threadMXBean);
        beansMap.put("garbageCollectorMXBeans", garbageCollectorMXBeans);
        beansMap.put("platformLoggingMXBean", platformLoggingMXBean);
        return beansMap;
    }

    public static String toJSON(Object obj) {
        // MemoryPoolMXBean these unset properties will cause serialization errors
        SimplePropertyPreFilter filter = new SimplePropertyPreFilter();
        filter.getExcludes().add("collectionUsageThreshold");
        filter.getExcludes().add("collectionUsageThresholdCount");
        filter.getExcludes().add("collectionUsageThresholdExceeded");
        filter.getExcludes().add("usageThreshold");
        filter.getExcludes().add("usageThresholdCount");
        filter.getExcludes().add("usageThresholdExceeded");
        //
        String jsonString = JSON.toJSONString(obj, filter, 
                SerializerFeature.PrettyFormat);
        return jsonString;
    }
}

Once these MXBeans are obtained, you can collect the corresponding Java runtime information and periodically report it to a certain system, thus creating a simple monitoring system.

Of course, for such a simple task, there are ready-made solutions available. For example, Spring Boot Actuator and Micrometer, which will be introduced later. Monitoring service providers’ Agent-lib also collect relevant data using similar methods.

If you want to obtain MXBeans on a remote machine programmatically, please refer to:

Using the Platform MBean Server and Platform MXBeans

Remote Connection using JMX Tools #

The most common JMX clients are JConsole and JVisualVM (which can install various plugins and is very powerful). Both tools are part of the standard JDK and are easy to use. If you are using JDK 7u40 or later, you can also use another tool called Java Mission Control (JMC).

Monitoring a local JVM does not require any additional configuration. For remote monitoring, you can deploy the Jstatd service on the server to expose some information to the JMX client.

All JMX clients are standalone programs that can connect to the target JVM. The target JVM can be on the local machine or a remote JVM.

To support connection from a JMX client to a server-side JVM instance, you need to add relevant configuration parameters to the Java startup script. Here is an example:

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=10990
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

If the server has multiple network interfaces (multiple IPs), you must explicitly specify the hostname due to security restrictions. Usually, it is the IP address.

-Djava.rmi.server.hostname=47.57.227.67

After starting with these configurations, JMX clients such as JConsole, JVisualVM, and JMC can connect using <IP:port> (referring to the example of JVisualVM).

For example, it would be something like: 47.57.227.67:10990.

If you want to remotely view VisualGC, the server needs to enable Jstatd support. First, connect JVisualVM to the remote host with Jstatd, and then right-click to add a JMX connection on the remote host. Please refer to the earlier article “Built-in Graphical Tools in JDK” for how to use JVisualVM.

Taking JConsole as an example, let’s see what MBean information can be viewed after connecting to a remote JVM in the last panel.

For example, we can view some information about the JVM:

enter image description here

We can also directly invoke methods, such as viewing VM arguments:

enter image description here

If the process started is Tomcat or a Spring Boot application with an embedded Tomcat, we can also see a lot of Tomcat information:

enter image description here

Creating and Accessing JMX MBeans Remotely #

In the previous section, we discussed how to obtain an MBean within the same JVM. Now let’s write a more complete example: creating an MBean and then accessing it remotely.

First, let’s define a UserMBean interface (which must have “MBean” as a suffix):

package io.github.kimmking.jvmstudy.jmx;

public interface UserMBean {
    Long getUserId();
    String getUserName();
    void setUserId(Long userId);
    void setUserName(String userName);
}

Next, let’s implement it:

package io.github.kimmking.jvmstudy.jmx;

public class User implements UserMBean {
    Long userId = 12345678L;
    String userName = "jvm-user";

    @Override
    public Long getUserId() {
        return userId;
    }

    @Override
    public String getUserName() {
        return userName;
    }

    @Override
    public void setUserId(Long userId) {
        this.userId = userId;
    }

    @Override
    public void setUserName(String userName) {
        this.userName = userName;
    }
}

Finally, let’s implement a class to start the MBeanServer:

package io.github.kimmking.jvmstudy.jmx;

import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.lang.management.ManagementFactory;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserJmxServer {
    public static void main(String[] args){
        MBeanServer server;
        User bean = new User();

        try {
            int rmiPort = 1099;
            String jmxServerName = "TestJMXServer";

            Registry registry = LocateRegistry.createRegistry(rmiPort);
            server = MBeanServerFactory.createMBeanServer("user");

            ObjectName objectName = new ObjectName("user:name=User");
            server.registerMBean(bean, objectName);

            JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/user");
            System.out.println("JMXServiceURL: " + url.toString());
            JMXConnectorServer jmxConnServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, server);
            jmxConnServer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

From these few lines of code, we can see that using the MBean mechanism requires the following steps:

  • Define an MBean interface.
  • Implement the interface.
  • Register the interface and class with an MBeanServer. We can use the default MBeanServer in the JVM or create a new Server. In this case, for simplicity, we use the default server.

Once we have done this, we can use client tools or code to access the MBeanServer, view, and manipulate MBeans. Thanks to the reflection-like nature of MBeans (if you have done COM object development on the Windows platform, you will find it similar), the client does not need to know the specific MBean interface or implementation class. It can still make requests to the server.

If you have studied Apache Dubbo, you know that in Dubbo, the consumer side needs to obtain the service interface of the service provider in order to configure and invoke it. The difference here is that the client does not need the MBean interface.

Viewing Custom MBeans in JConsole #

First, let’s start the UserJmxServer application. Next, let’s use a tool to view and manipulate it.

Open JConsole and enter the following address in “Remote”:

service:jmx:rmi:///jndi/rmi://localhost:1099/user

Viewing User Attributes:

enter image description here

Modifying the value of UserName directly:

enter image description here

Accessing MBean Remotely using JMX #

First, we use JMXUrl to create an MBeanServerConnection and connect to the MBeanServer. Then, we can retrieve the attributes of the MBean on the server side using the ObjectName, which can be seen as the address of the MBean, just like using reflection. We can also call methods of the MBean. The code example is as follows:

package io.github.kimmking.jvmstudy.jmx;

import javax.management.*;
import javax.management.remote.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Iterator;
import java.util.Set;

public class UserJmxClient {
    public static void main(String[] args){
        try {
            String surl = "service:jmx:rmi:///jndi/rmi://localhost:1099/user";
            JMXServiceURL url = new JMXServiceURL(surl);
            JMXConnector jmxc = JMXConnectorFactory.connect(url, null);
            MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();

            System.out.println("Domains:---------------");
            String domains[] = mbsc.getDomains();
            for (int i = 0; i < domains.length; i++) {
                System.out.println("\tDomain[" + i + "] = " + domains[i]);
            }
            System.out.println("all ObjectName:---------------");
            Set<ObjectInstance> set = mbsc.queryMBeans(null, null);
            for (Iterator<ObjectInstance> it = set.iterator(); it.hasNext();) {
                ObjectInstance objectInstance = (ObjectInstance) it.next();
                System.out.println("\t" + objectInstance.getObjectName() + " => " + objectInstance.getClassName());
            }
            System.out.println("user:name=User:---------------");
            ObjectName mbeanName = new ObjectName("user:name=User");
            MBeanInfo info = mbsc.getMBeanInfo(mbeanName);
            System.out.println("Class: " + info.getClassName());
            if (info.getAttributes().length > 0){
                for(MBeanAttributeInfo m : info.getAttributes())
                    System.out.println("\t ==> Attribute:" + m.getName());
            }
            if (info.getOperations().length > 0){
                for(MBeanOperationInfo m : info.getOperations())
                    System.out.println("\t ==> Operation:" + m.getName());
            }

            System.out.println("Testing userName and userId .......");
            Object userNameObj = mbsc.getAttribute(mbeanName,"UserName");
            System.out.println("\t ==> userName:" + userNameObj);
            Object userIdObj = mbsc.getAttribute(mbeanName,"UserId");
            System.out.println("\t ==> userId:" + userIdObj);

            Attribute userNameAttr = new Attribute("UserName","kimmking");
            mbsc.setAttribute(mbeanName,userNameAttr);

            System.out.println("Modify UserName .......");

            userNameObj = mbsc.getAttribute(mbeanName,"UserName");
            System.out.println("\t ==> userName:" + userNameObj);

            jmxc.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

When running the above code, the output is as follows:

Domains:---------------
 Domain[0] = JMImplementation
 Domain[1] = user
all ObjectName:---------------
 JMImplementation:type=MBeanServerDelegate => javax.management.MBeanServerDelegate
 user:name=User => io.github.kimmking.jvmstudy.jmx.User
user:name=User:---------------
Class: io.github.kimmking.jvmstudy.jmx.User
  ==> Attribute:UserName
  ==> Attribute:UserId
Testing userName and userId .......
  ==> userName:jvm-user
  ==> userId:12345678
Modify UserName .......
  ==> userName:kimmking

In the previous JConsole example, we can see that all MBeans in the MBeanServer of JMX form a tree structure. To locate an MBean object, we use its address, namely the ObjectName property, such as user:name=User in the example. ObjectName is very similar to DN in LDAP, and it allows us to directly obtain a proxy object of the actual server-side object on the client side. Then we can perform operations on it:

  • queryMBeans: Query all MBean objects on the current Server and obtain the MBeanInfo information of each MBean, which includes its attributes and methods.
  • getAttribute: Retrieve a specific attribute value of an MBean object on the Server.
  • setAttribute: Set the value of a specific attribute of an MBean on the Server.
  • invoke: Invoke a specific method of an MBean on the Server.

From the above analysis, we can see that JMX is actually a very well-designed remote invocation technology based on the MBean and MBeanServer model and the RMI protocol. By studying the details of this technology, we can understand other RPC technologies as well. With this default management API technology of the JVM, we can also more easily understand and analyze the situation of the JVM.

More Usages #

JMX is a JVM management technology based on RMI (Remote Method Invocation), which is a Java platform-specific protocol. It is difficult to achieve cross-language invocation. How can we achieve cross-platform communication? The most popular remote invocation method nowadays is REST. Can JMX be used to directly invoke REST APIs? The answer is yes.

In addition, if we want to conduct performance analysis, JVM information alone is not enough. We also need to integrate with other monitoring systems, such as Datadog or other APM (Application Performance Monitoring) tools. This article only briefly touches on this topic.

JMX and REST API #

Let’s first talk about the REST API of JMX. There is a framework called Jolokia, which can automatically convert the structure of JMX into REST API and JSON data. This framework is used by default in the control panel of open-source software ActiveMQ, which achieves the following effect directly.

If we use curl to manually execute a REST call, the API will directly return the JSON result to us.

$ curl http://localhost:8161/hawtio/jolokia/read/org.apache.activemq:brokerName=localhost,type=Broker/Queues

{"timestamp":1392110578,"status":200,"request":{"mbean":"org.apache.activemq:brokerName=localhost,type=Broker","attribute":"Queues","type":"read"},"value":[{"objectName":"org.apache.activemq:brokerName=localhost,destinationName=a,destinationType=Queue,type=Broker"}]}

For more information, please refer to the reference materials.

JMX and Other Software #

Tools like JConsole and JVisualVM provide real-time monitoring capabilities. But what should we do if we want to monitor historical data for a large number of JVM instances?

Since JMX provides this data, as long as we have a tool to collect it periodically and report it to the corresponding APM collection system, we can store long-term historical data for further analysis and performance diagnostics.

For example, service providers like DataDog and Dynatrace have integrated support for JMX.

Since our column mainly focuses on the usage of JDK-related tools, for those who want to learn more, please search using keywords like “Datadog JMX” or “Dynatrace JMX”, etc.

If you search for “Spring JMX”, you may even see how JMX can be used in a variety of ways. However, compared to HTTP APIs, JMX is relatively heavyweight. Therefore, for enterprises and engineers with programming skills, if they want flexibility and convenience, using HTTP interfaces is the most convenient way.