Saturday, March 23, 2013

Monitoring Key JVM Characteristics with Groovy, JMX, and RuntimeMXBean

Since J2SE 5, Platform MBeans have been available that allow some key characteristics regarding the JVM to be monitored and (even managed in some cases) via JMX. In addition, many JVM-based applications add their own JMX-enabled features for monitoring and management. In the blog post Groovy, JMX, and the Attach API, I looked at how to display many of the platform-provided MBeans using Groovy, JMX, and the Attach API. In this post, I look at more specific scripts that access a narrowly focused subset of these platform-exposed values available in the RuntimeMXBean.

My previous post demonstrated ability to view a wide spectrum of JVM details with Groovy and JMX. In most cases, I don't need all of these details all at once, but simply need one or two of the items. Although I could use a tool such as JConsole, VisualVM, or even a modern Java IDE to see these details, I sometimes wish to run a simple script to provide the exact results I'm looking for rather than using a general JMX client that I need to start up and navigate to that information. This is where simple scripts such as shown in this post are particularly handy.

All of my examples in this post assume the person running the Java process to be monitored/managed is the same person (same username) running the scripts and that they are being used to monitor Java processes running on the same local machine. This assumption allows the Attach API to be used. If a different user was running the script than ran the Java processes or if the scripts were to be run on remote Java processes, techniques for remote JMX would be used instead. Because the Attach API will be used in these examples, I have placed the code that each example uses in a Groovy script file that each example invokes. That file to be used by all of my scripts is called JmxServer.groovy and is shown next.

JmxServer.groovy
/*
 * JmxServer.groovy
 *
 * Functions meant to be called by other scripts or tools that need to use
 * the Attach API to access an MBeanServerConnection to use to manage and
 * monitor a particular JVM (and possibly the application hosted in that JVM)
 * identified by the provided Process ID (pid)
 */

import javax.management.MBeanServerConnection
import javax.management.ObjectName
import javax.management.remote.JMXConnector
import javax.management.remote.JMXConnectorFactory
import javax.management.remote.JMXServiceURL

import com.sun.tools.attach.VirtualMachine

/**
 * Provide an MBeanServerConnection based on the provided process ID (pid).
 *
 * @param pid Process ID of Java process for which MBeanServerConnection is
 *    desired.
 * @return MBeanServerConnection connecting to Java process identified by pid.
 */
def static MBeanServerConnection retrieveServerConnection(String pid)
{
   def connectorAddressStr = "com.sun.management.jmxremote.localConnectorAddress"
   def jmxUrl = retrieveUrlForPid(pid, connectorAddressStr)
   def jmxConnector = JMXConnectorFactory.connect(jmxUrl)
   return jmxConnector.getMBeanServerConnection()
}

/**
 * Provide JMX URL for attaching to the provided process ID (pid).
 *
 * @param @pid Process ID for which JMX URL is needed to connect.
 * @param @connectorAddressStr String for connecting.
 * @return JMX URL to communicating with Java process identified by pid.
 */
def static JMXServiceURL retrieveUrlForPid(String pid, String connectorAddressStr)
{
   // Attach to the target application's virtual machine
   def vm = VirtualMachine.attach(pid)

   // Obtain Connector Address
   def connectorAddress =
      vm.getAgentProperties().getProperty(connectorAddressStr)

   // Load Agent if no connector address is available
   if (connectorAddress == null)
   {
      def agent = vm.getSystemProperties().getProperty("java.home") +
          File.separator + "lib" + File.separator + "management-agent.jar"
      vm.loadAgent(agent)

      // agent is started, get the connector address
      connectorAddress =
         vm.getAgentProperties().getProperty(connectorAddressStr)
   }

   return new JMXServiceURL(connectorAddress);
}

The most important method defined in JmxServer.groovy, from a script client perspective, is the retrieveServerConnection(String) method. That methods excepts a pid as a process identifier and uses that and a call to the other method, retrieveUrlForPid(String,String) to attach to the Java process identified by that pid and provide an associated MBeanServerConnection instance. The easiest approaches for finding the appropriate Java pid are use of jps (pre-Java 7 and Java 7) or jcmd (Java 7+ only) as I briefly described in my recent post Booting AMX in GlassFish 3 with Groovy.

Having encapsulated the logic for acquiring a particular JVM (Java process's) MBeanServerConnection instance based on a provided pid, it is easy to start constructing simple Groovy scripts that use this provided MBeanServerConnection to provide specific desirable details about the JVM (Java process) in question. Note that all of these example scripts are in the same directory as the JmxServer.groovy file and so do not need to explicitly specify package or scoping details.

It is common to want to know what is on the classpath of a particular JVM for various reasons including detection of causes of ClassNotFoundException and NoClassDefFoundError. The classpath of a particular JVM is exposed by one of its platform MBean ("ClassPath" attribute of RuntimeMXBean) and can be easily discovered with the next Groovy script:

displayClasspath.groovy
#!/usr/bin/env groovy
def pid = args[0]
def javaRuntime = new javax.management.ObjectName("java.lang:type=Runtime")
def classpath = JmxServer.retrieveServerConnection(pid).getAttribute(javaRuntime, "ClassPath")
println "\nClasspath for Java pid (${pid}):\n${classpath}\n"

That same RuntimeMXBean also provides the boot classpath and the Java library path. The next two code listings show Groovy scripts for accessing both of those.

displayBootClasspath.groovy
#!/usr/bin/env groovy
def pid = args[0]
def javaRuntime = new javax.management.ObjectName("java.lang:type=Runtime")
def bootClasspath = JmxServer.retrieveServerConnection(pid).getAttribute(javaRuntime, "BootClassPath")
println "\nBoot Classpath for Java pid (${pid}):\n${bootClasspath}\n"
displayLibraryPath.groovy
#!/usr/bin/env groovy
def pid = args[0]
def javaRuntime = new javax.management.ObjectName("java.lang:type=Runtime")
def libraryPath = JmxServer.retrieveServerConnection(pid).getAttribute(javaRuntime, "LibraryPath")
println "\nLibrary Path for Java pid (${pid}):\n${libraryPath}\n"

The Groovy scripts shown so far for displaying details related finding classes (the classpath, boot classpath, and library path) are all essentially the same script, but with a different attribute looked up on the RuntimeMXBean in each case. All three scripts are executed against a running instance of GlassFish in the following screen snapshot. The snapshot includes commands to jps and jcmd to determine the pid (5352) for the GlassFish instance.

The RuntimeMXBean has more to offer than just information on where classes were loaded from. It also includes attributes related to the JVM vendor such as name and version, but the other three methods that I like to have scripts to use are input arguments, system properties, and JVM uptime (and start time). The next three Groovy code listings show three scripts for displaying these details and each code snippet is followed by a screen snapshot demonstrating that script in action against the same GlassFish instance used in the last examples.

displayInputArguments.groovy
#!/usr/bin/env groovy
def pid = args[0]
def javaRuntime = new javax.management.ObjectName("java.lang:type=Runtime")
def inputArguments = JmxServer.retrieveServerConnection(pid).getAttribute(javaRuntime, "InputArguments")
println "\nInput Arguments for Java pid (${pid}):\n${inputArguments}\n"
displaySystemProperties.groovy
#!/usr/bin/env groovy
def pid = args[0]
def javaRuntime = new javax.management.ObjectName("java.lang:type=Runtime")
def systemProperties = JmxServer.retrieveServerConnection(pid).getAttribute(javaRuntime, "SystemProperties")
println "\nSystem Properties for Java pid (${pid}):\n${systemProperties}\n"
displayJvmUptime.groovy
#!/usr/bin/env groovy
def pid = args[0]
def javaRuntime = new javax.management.ObjectName("java.lang:type=Runtime")
def server = JmxServer.retrieveServerConnection(pid)
def startTime = server.getAttribute(javaRuntime, "StartTime")
def uptime = server.getAttribute(javaRuntime, "Uptime")
println "\nJava process pid (${pid}) was started at ${new Date(startTime)} and has been up for ${uptime} ms.\n"

I have used this post to demonstrate simple Groovy scripts that acquire information from a JVM's Platform RuntimeMXBean such as the JVM's start time and uptime, the system properties and input arguments for the JVM process, and path information such as classpath, boot classpath, and library path. Although this information is available via tools such as JConsole, VisualVM, and Java IDEs (they all get it from the same source as these scripts!), the scripts can be advantageous at times (quicker to run and can be included within other scripts, for example).

No comments: