JMX Redux

Dr. Dobb's Journal July, 2005

Web-service access & JVM management in Java 5.0

By Juan C. Due–as, Manuel Santillan, and Jose Luis Ruiz

Juan Carlos is an associate professor and Jose Luis and Manuel are graduate researchers at the Universidad Politécnica de Madrid, Spain. They can be contacted at jcduenas@dit.upm.es, jlruiz@dit.upm.es, and santillan@dit.upm.es, respectively.

Java Management Extensions (JMX) is the management standard both in J2EE and J2SE. Because JMX greatly simplifies the instrumentation of resources and remote access to them, it is becoming the de facto standard for Java application and services management. Java 5.0 (http://java.sun.com/j2se/1.5.0/index.jsp) includes the JMX 1.2 specification together with the remote API, as well as virtual-machine-level instrumentation that enables off-the-shelf virtual-machine (VM) monitoring and management. Moreover, JMX management capabilities can be leveraged by using a number of open-source tools that provide management consoles and communications infrastructure. You can use JMX in Java 5.0 VMs with different communication connectors and adapters to provide a multiprotocol view of your applications and harness the benefit from the existing consoles to monitor and control your applications, services, and resources in production environments.

In this article, we build on "Java Management Extensions" by Paul Tremblett (DDJ, July 2004), which focused on how to set up a management agent and use an HTTP adapter to remotely interact with it. We complement that article by showing how you can access MBeans from your own code. Additionally, we provide an in-depth view of Java's management technology, and describe the monitoring and management information included in Java 5.0. Finally, we create a web services text-based management console, and present MC4J, an open-source, visual management console for JMX that uses RMI. Figure 1 illustrates how all these ideas fit together.

What Can Be Monitored and Managed in the JVM?

Until now, performance measurements were quite complicated and relied on native profilers for the retrieval of information. The most recent Java specification, however, includes some low-level monitoring and management capabilities. The new java.lang.management package, for instance, defines an easy-to-use API to deal with these issues. This package contains a number of useful managed beans that let you invoke management operations on, obtain information about, and receive notifications from a running VM.

Among the different monitoring and management capabilities this API provides are:

How Do You Monitor and Manage the JVM?

VM-related information and operations are exposed via MBeans. As with any other MBeans, you can access them in different ways. Here, we focus on local access to the management information (that is, from within the same VM) and how you can remotely interact with the managed beans afterward.

The first way of accessing the MBeans is using a reference to the managed object (as with any regular object). In particular, with the VM-related MBeans, you can use the ManagementFactory static methods to get a reference to them. This is exactly what we do in Example 1 to get a reference to the MemoryMXBean.

So, what's the big deal about JMX? Nothing, if you have a reference to the object. However, this is not always the case. The MBean server can be used as an object registry that enables searching for managed objects. Moreover, remember that we are also going to remotely access this information in an easy way.

A second way to access the managed object is using the MBean server capabilities. This implies using reflection-like syntax, which may be a little annoying to code and read afterward; see Example 2. The great thing about doing it this way is that you can use the searching capabilities of the MBean server to look for objects, even if you do not know their names.

You can get the best of both worlds by coding a proxy that implements the MXBean interface and uses reflection syntax to access the MBeans. In this way, you abstract the details of interacting with the MBean server. This is the strategy in Example 3, where we use a proxy to connect to the MemoryMXBean. In fact, what we are doing is the same as in Example 2 (only with one more level of indirection), but it looks exactly the same as in Example 1. You can see in Listing One the full content of the MemoryProxy and how it uses the Inversion Of Control pattern, so that it can be used with remote MBean servers as well.

Remotely Interacting With the MBean Server

The JMX architecture provides two basic entities for remote access to the MBean server—adapters and connectors. Adapters provide a protocol-specific view of the MBean server and the existing MBeans. Typical examples of adapters include SNMP and HTTP. The SNMP adapter translates JMX information and exposes it so that the client will think that he is talking to an SNMP agent, not to a JMX one. As in Paul Tremblett's article, the HTTP adapter provides a web interface to easily operate your application from a common web browser.

The alternative is to use connectors, which are formed by a pair-connector/ server-connector client. The connector server runs in the same VM as the MBean server. The connector client may run on a different VM and, possibly, in a different host. JMX connectors deal with all the communications-related complexity and present a familiar interface to users—the MBeanServerConnection. This interface is a super interface of the MBeanServer, and lets you interact with it as if you were inside the same VM.

A useful feature of connectors is that changing one connector to another is effortless. As you can see in Example 4, the code is nearly the same as in Example 2; and Example 5 is nearly the same as Example 3. In both cases, the only difference relies on how we get a reference to the MBeanServerConnection: In Examples 2 and 3, we use the ManagementFactory to get a reference to the platform MBean server; while in Examples 4 and 5, we use a connector to get the (remote) reference.

For the examples to work, we also need to have a remote connector server running in the same VM as the MBean server we want to access; see Example 6. In this example, you can see how easy it is to set it up. You just have to provide a valid JMXServiceURL to the connector server factory, which does all the work for you. You need to have an RMI registry running on the specified port (1099 in the example) where the factory can register the stub. In our example, the protocol is "rmi," the stub is registered under the path "/server," and the machine running the RMIRegistry is "alfheim.dit.upm.es."

Again, JMX connectors are easily interchangeable. To have a totally different transport, you only need to change the connector server's URL and the connector client's one accordingly. In this way, JMX gives you flexibility in the transport you want to use.

What Can I Use the JVM Management Information For?

Some of the information exposed is obviously interesting in its own right, such as the operating system that your application is running on, or the runtime information. However, there are a few things you can do with it:

Imagine that you have an application that stores both important data and also some unimportant data (maybe cached data for performance purposes) in the heap. In this situation, you would like to keep the unimportant data in the heap for as long as the space it uses is not necessary, so that your application performs nicely. However, you would not like your application to crash for not having enough space for the important data. You would like your application to dynamically decide whether to remove the unimportant data from memory depending on the amount of available heap space. A way of implementing this is by defining a memory threshold that will be used by your application to make the decision: When surpassed, it automatically removes the unimportant data from the heap.

In Listing Two, you can see an example of this kind of behavior. We have a thread that periodically adds information, both important and unimportant, to the heap. We have set a threshold to the Tenured Gen memory pool (this is where the long-living objects are placed in Sun's VM reference implementation). We have added a notification listener to the MemoryMXBean so that whenever the threshold is exceeded, our listener will be called, and it will remove the unimportant data from the heap.

Managing Java Apps with Web Services

One of the great features of using JMX as management technology is its independence from the underlying transport protocols. You can take advantage of this capability using web services. This lets you manage your applications using SOAP/XML streamed over HTTP. Because many organizations provide web-service interfaces for their applications, reusing the same technologies for application management reduces the effort to protect and maintain the assets.

To show how to do this, we create a SOAP console using MX4J's SOAP connector. MX4J (http://mx4j.sourceforge.net/) is an open-source implementation of the JMX specification, which provides some extra functionality. This functionality is fully compatible with Sun's reference implementation, as in our sample text console, where we use Java 5.0 and its included JMX implementation along with MX4J SOAP connectors.

In Listings Three and Four, we present a simple text-based console that uses a SOAP connector to remotely interact with an MBean server. More precisely, Listing Three shows how to set up a SOAP connector server that listens to incoming calls, while Listing Four implements the actual management console.

When we start the server-side connector in Listing Three, a Jetty web container (http://jetty.mortbay.com/jetty/index.html) is created at the specified port. Afterwards, the Apache Axis servlet (http://ws.apache.org/axis/) is deployed to it, and a web service representing the MBean server is deployed to Axis. You may find it useful that you can also use a running container to deploy the connector. This can be used, for example, to add JMX-SOAP support to your web applications.

On the client side (Listing Four), the console connects to the remote MBean server and prints a list of the actual MBeans names available. An interesting variation of this console would be to have a remote monitor that periodically polls your application and automatically reacts to predefined events; for example, sending an e-mail to you.

To create the necessary connections to the server side, the client-side connector uses the information contained in the JMXServiceURL. In this sense, you should take into account an important implementation detail: The host name of both client- and server-side JMXServiceURLs needs to be exactly the same. If not (maybe because one uses "localhost" and the other uses "127.0.0.1"), then the client will not be able to find the server.

The main drawback when using the SOAP connector is inherent to Axis: If you want to send nonbasic nonJMX objects over the wire, you must implement a serializer/deserializer pair that handles the details and add it to Axis.

Using Existing GUI Consoles

You have seen how easily you can create your own management console. However, in most cases, you would probably make do with an existing console. The best aspect of reusing an existing console is that you don't need to bother with client-side code. Of course, you lose some amount of flexibility.

When choosing your management console, there are some alternatives. Here, we present MC4J (http://mc4j.sourceforge.net/), an open-source tool based on Sun's NetBeans IDE (http://www.netbeans.org/). MC4J focuses on managing J2EE servers such as Geronimo and Jboss, and therefore, includes support for JSR 77's J2EE management. However, you can use MC4J to remotely manage any JSR 160-compliant MBean server, as long as the protocol used is the mandatory RMI one. We have already seen (in Example 6) how to set up an RMI connector server inside your MBean server.

On the client side, you only need to tell the MC4J console which type of connection you are using at the other end and where to locate it. In fact, once you indicate that it is a JSR160 connection, the console tries to guess the correct URL (as it only supports RMI). You only need to modify the port and the host of the RMI registry if they are not correct, as in Figure 2. In Figure 3, you can see a running example of the console and its management capabilities.

Moreover, you can further extend MC4J to meet your needs. Not only can you extend it by modifying the application's source code, but it also supports a certain degree of pluggability; for example, you can define your own dashboards to get a more application-specific management view.

DDJ



Listing One
package ddjarticle;

import java.lang.management.*;
import javax.management.*;
import javax.management.openmbean.*;

/**
 * Listing One A proxy for the MemoryMXBean
 * 
 * @author Manuel Santillan
 *  
 */
public class MemoryProxy implements MemoryMXBean {
    private MBeanServerConnection server;

    private ObjectName oname;

    public MemoryProxy(MBeanServerConnection server) throws Exception {
        this.server = server;
        this.oname = ObjectName
                .getInstance(ManagementFactory.MEMORY_MXBEAN_NAME);
    }

    public int getObjectPendingFinalizationCount() {
        try {
            return ((Integer) server.getAttribute(oname,
                    "ObjectPendingFinalizationCount")).intValue();
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

    public MemoryUsage getHeapMemoryUsage() {
        try {
            Object usage = server.getAttribute(oname, "HeapMemoryUsage");
            return MemoryUsage.from((CompositeData) usage);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public MemoryUsage getNonHeapMemoryUsage() {
        try {
            Object usage = server.getAttribute(oname, "NonHeapMemoryUsage");
            return MemoryUsage.from((CompositeData) usage);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public boolean isVerbose() {
        try {
            return ((Boolean) server.getAttribute(oname, "Verbose"))
                    .booleanValue();
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public void setVerbose(boolean value) {
        try {
            Attribute at = new Attribute("Verbose", new Boolean(value));
            server.setAttribute(oname, at);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void gc() {
        try {
            server.invoke(oname, "gc", null, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Back to article


Listing Two
package ddjarticle;

import java.lang.management.*;
import javax.management.*;
import javax.management.openmbean.*;
import java.util.*;

/**
 * <p>
 * This is a memory policy example. This code creates "important data" and
 * "unimportant data". When threshold is exceeded, unimportant data is removed.
 * </p>
 * 
 * @author Manuel Santillan
 *  
 */
public class ListingTwo_MemoryPolicy implements Runnable, NotificationListener {
    public static final long MEMORY_THRESHOLD = 10000000;

    private Hashtable importantObjects;

    private Hashtable unimportantObjects;

    private int key;

    private byte[] unimportantDataUnit;

    private byte[] importantDataUnit;

    public ListingTwo_MemoryPolicy() {
        importantObjects = new Hashtable();
        unimportantObjects = new Hashtable();
        key = 0;
        this.unimportantDataUnit = new byte[100 * 1024];
        this.importantDataUnit = new byte[1024];
    }

    public void run() {
        while (true) {
            try {
                //Each millisecond we create 100KB of unimportant data and 1KB
                // of important data.
                this.addData();
                Thread.sleep(1);
            } catch (InterruptedException ie) {
            }
            key++;
        }
    }

    public void handleNotification(Notification notification, Object handback) {
        if (notification.getType().equals(
                MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED)) {
            MemoryNotificationInfo info = MemoryNotificationInfo
                    .from((CompositeData) notification.getUserData());
            MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
            long current = memory.getHeapMemoryUsage().getUsed();
            System.out.println("THRESHOLD_EXCEEDED notification received");
            System.out.println("Current memory used: " + current
                    + " and threshold: " + MEMORY_THRESHOLD);
            System.out.println("Freeing unimportant memory...");
            clean();
            System.gc();
            current = memory.getHeapMemoryUsage().getUsed();
            System.out.println("Current: " + current);
            if (current < MEMORY_THRESHOLD) {
                System.out.println("done");
            } else
                System.out.println("Could not free enough memory. ");
        }
    }

    private synchronized void addData() {
        this.importantObjects.put(new Integer(key), importantDataUnit.clone());
        this.unimportantObjects.put(new Integer(key), unimportantDataUnit
                .clone());
    }

    private synchronized void clean() {
        this.unimportantObjects.clear();
    }

    public static void main(String[] args) throws Exception {
        ListingTwo_MemoryPolicy example = new ListingTwo_MemoryPolicy();
        Thread t = new Thread(example);
        MBeanServerConnection server = ManagementFactory
                .getPlatformMBeanServer();
        NotificationFilterSupport filter = new NotificationFilterSupport();
        filter.enableType(MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED);
        server.addNotificationListener(ObjectName
                .getInstance(ManagementFactory.MEMORY_MXBEAN_NAME), example,
                filter, null);
        ListingTwo_MemoryPolicy.getTenuredGenPool().setUsageThreshold(
                MEMORY_THRESHOLD);
        t.start();
    }

    private static MemoryPoolMXBean getTenuredGenPool() {
        //In the reference implementation, the only heap memory space that
        // supports threshold is the tenured gen one
        //this is reasonable, as the other ones are for very short lived
        // objects
        List list = ManagementFactory.getMemoryPoolMXBeans();
        MemoryPoolMXBean mp;
        MemoryPoolMXBean tenured = null;
        for (int i = 0; i < list.size(); i++) {
            mp = (MemoryPoolMXBean) list.get(i);
            if (mp.getName().equals("Tenured Gen"))
                tenured = mp;
        }
        return tenured;
    }
}
Back to article


Listing Three
package ddjarticle;

import java.lang.management.ManagementFactory;
import java.util.HashMap;
import java.util.Map;

import javax.management.MBeanServer;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;

/**
 * <p>
 * It starts a simple SOAP connector server. In order to have it working
 * properly, you need to have in your classpath:
 * </p>
 * <ul>
 * <il>mx4j-tools.jar from <a href="http://mx4j.sourceforge.net">MX4J. /a> You
 * might need to recompile with a Axis 1.2 to have it working in Java 5.0 </il>
 * <il>axis.jar version 1.2 and all its related jars </il>
 * <il>org.mortbay.jetty.jar and related </il>
 * </ul>
 * 
 * @author Manuel Santillan DIT-UPM
 */
public class SOAPserver {
    /**
     * This class is an example using mx4j's soap connector. It implements the
     * server part. the SOAPClient class implements a simple example client We
     * use java 1.5, so we have recompiled mx4j with axis1.2rc1
     */
    public static void main(String[] args) throws Exception {
        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        Map environment = new HashMap();
        //We need to specify the provider packages whereJMX will search for the
        // protocols.
        //If not, it won't find the SOAP connector
        //We can use an existing axis framework. To do that, you must add some
        // other properties to the environment.
        //See MX4J documentation (http://mx4j.sourceforge.net) for details.
        environment.put(JMXConnectorServerFactory.PROTOCOL_PROVIDER_PACKAGES,
                "mx4j.tools.remote.provider");
        JMXServiceURL url = new JMXServiceURL("soap", "cauca.dit.upm.es", 8080,
                "/jmxconnector");
        JMXConnectorServer conn = JMXConnectorServerFactory
                .newJMXConnectorServer(url, environment, server);
        conn.start();
    }
}
Back to article


Listing Four
package ddjarticle;

import java.util.*;
import java.io.*;
import javax.management.*;
import javax.management.remote.*;

import org.apache.axis.enum.Enum;

/**
 * Simple text-based SOAP console that accepts just one command
 * 
 * @author Manuel Santillan
 * 
 *  
 */
//What you need to run these examples
public class SOAPConsole implements Runnable {
    private MBeanServerConnection server;

    public static final String COMMAND_NAME = "list";

    private SOAPConsole(MBeanServerConnection server) {
        this.server = server;
    }

    public static void main(String[] args) throws Exception {
        //First we set the connector, with the *SAME* URL as the server
        Map environment = new HashMap();
        environment.put(JMXConnectorServerFactory.PROTOCOL_PROVIDER_PACKAGES,
                "mx4j.tools.remote.provider");
        JMXServiceURL url = new JMXServiceURL("soap", "cauca.dit.upm.es", 8080,
                "/jmxconnector");
        JMXConnector cntor = JMXConnectorFactory.connect(url, environment);
        //Then we get a connection which behaves as if we were inside remote
        // the MBeanServer
        SOAPConsole console = new SOAPConsole(cntor.getMBeanServerConnection());
        Thread t = new Thread(console);
        t.start();
    }

    private void printMBeans(ObjectName filter) throws Exception {
        //Our console is very simple.
        //It has just one command: listobjects (String filtername)
        Iterator it = server.queryNames(filter, null).iterator();
        System.out.println("The MBeans that match your query are: ");
        while (it.hasNext()) {
            System.out.println("\t" + it.next());
        }
    }

    public void run() {
        while (true) {
            try {
                BufferedReader br = new BufferedReader(new InputStreamReader(
                        System.in));
                String line = br.readLine();
                String[] command = line.split(" ");
                //We only support one command
                if (command[0].equalsIgnoreCase("exit"))
                    System.exit(0);
                if (!command[0].equalsIgnoreCase(SOAPConsole.COMMAND_NAME)
                        || command.length > 2) {
                    System.out.println("Syntax: list [filter] or exit");
                    continue;
                }
                //If length =1 then filter=null
                if (command.length == 1)
                    printMBeans(null);
                else
                    printMBeans(ObjectName.getInstance(command[1]));
            } catch (MalformedObjectNameException moe) {
                System.out
                        .println("Invalid filter. check javax.management.ObjectName for correct filter syntax.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
Back to article