Monday, January 31, 2011

Groovy, JMX, and the Attach API

One of the exciting new features of the (then Sun) HotSpot Java SE 6 release was support for the Attach API. The Attach API is "a Sun Microsystems extension that provides a mechanism to attach to a Java virtual machine." Tools such as JConsole and VisualVM "use the Attach API to attach to a target virtual machine and load its tool agent into that virtual machine." Custom Java and Groovy clients can likewise use the Attach API to monitor and manage JVMs.

There are several online resources that demonstrate Java client code that uses the Attach API. These include Daniel Fuchs's code listing for a JVMRuntimeClient, the "Setting up Monitoring and Management Programmatically" section of "Monitoring and Management Using JMX Technology," the Core Java Technology Tech Tip called "The Attach API," and the Javadoc API documentation for the class com.sun.tools.attach.VirtualMachine. These examples generally demonstrate using VirtualMachine.attach(String) to attach to the virtual machine based on its process ID in String form. This is generally followed by loading the appropriate agent with VirtualMachine.loadAgent(String), where the String parameter represents the path to the JAR file containing the agent. The VirtualMachine.detach() method can be called to detach from the previously attached JVM.

All of the previously mentioned examples demonstrate use of the Attach API from Java clients. In this post, I demonstrate use of the Attach API via Groovy. The three code listings that follow present three pieces of Groovy code but all work together as a single script. The main script is embodied in the Groovy file getJvmThreadInfo.groovy and is a simple script file that calls the other two Groovy script files (attachToVirtualMachine.groovy and displayMxBeanDerivedInfo.groovy) to attach to the virtual machine and to display details regarding that virtual machine via its MXBeans.

getJvmDetails.groovy
#!/usr/bin/env groovy
// getJvmDetails.groovy
//
// Main script for extracting JVM details via Attach API and JMX.
// Accepts single parameter which is the process ID (pid) of the Java application
// whose JVM is to be connected to.
//
import static attachToVirtualMachine.retrieveConnector
import static displayMxBeanDerivedInfo.*

def serverConnection = attachToVirtualMachine.retrieveServerConnection(args[0])

displayMxBeanDerivedInfo.displayThreadInfo(serverConnection)
displayMxBeanDerivedInfo.displayOperatingSystemInfo(serverConnection)
displayMxBeanDerivedInfo.displayRuntimeInfo(serverConnection)
displayMxBeanDerivedInfo.displayMemoryInfo(serverConnection)

attachToVirtualMachine.groovy
// attachToVirtualMachine.groovy
//
// Provide an MBeanServerConnection acquired via the Attach API.

import javax.management.MBeanServerConnection
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)
{
   println "Get JMX Connector for pid ${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);
}

displayMxBeanDerivedInfo.groovy
// displayMxBeanDerivedInfo.groovy
//
// Display details regarding attached virtual machine and associated MXBeans.

import java.lang.management.ManagementFactory
import java.lang.management.MemoryMXBean
import java.lang.management.OperatingSystemMXBean
import java.lang.management.RuntimeMXBean
import java.lang.management.ThreadMXBean
import javax.management.MBeanServerConnection

/**
 * Display thread information based on ThreadMXBean associated with the provided
 * MBeanServerConnection.
 *
 * @param server MBeanServerConnection to use for obtaining thread information
 *    via the ThreadMXBean.
 */
def static void displayThreadInfo(MBeanServerConnection server)
{
   def remoteThreadBean = ManagementFactory.newPlatformMXBeanProxy(
                             server,
                             ManagementFactory.THREAD_MXBEAN_NAME,
                             ThreadMXBean.class);

   println "Deadlocked Threads: ${remoteThreadBean.findDeadlockedThreads()}"
   println "Monitor Deadlocked Threads: ${remoteThreadBean.findMonitorDeadlockedThreads()}"
   println "Thread IDs: ${Arrays.toString(remoteThreadBean.getAllThreadIds())}"
   def threads = remoteThreadBean.dumpAllThreads(true, true);
   threads.each
   {
      println "\t${it.getThreadName()} (${it.getThreadId()}): ${it.getThreadState()}"
   }
}


/**
 * Display operating system information based on OperatingSystemMXBean associated
 * with the provided MBeanServerConnection.
 *
 * @param server MBeanServerConnection to use for obtaining operating system
 *    information via the OperatingSystemMXBean.
 */
def static void displayOperatingSystemInfo(MBeanServerConnection server)
{
   def osMxBean = ManagementFactory.newPlatformMXBeanProxy(
                     server,
                     ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME,
                     OperatingSystemMXBean.class)
   println "Architecture: ${osMxBean.getArch()}"
   println "Number of Processors: ${osMxBean.getAvailableProcessors()}"
   println "Name: ${osMxBean.getName()}"
   println "Version: ${osMxBean.getVersion()}"
   println "System Load Average: ${osMxBean.getSystemLoadAverage()}"
}


/**
 * Display operating system information based on RuntimeMXBean associated with
 * the provided MBeanServerConnection.
 *
 * @param server MBeanServerConnection to use for obtaining runtime information
 *    via the RuntimeMXBean.
 */
def static void displayRuntimeInfo(MBeanServerConnection server)
{
   def remoteRuntime = ManagementFactory.newPlatformMXBeanProxy(
                          server,
                          ManagementFactory.RUNTIME_MXBEAN_NAME,
                          RuntimeMXBean.class);

   println "Target Virtual Machine: ${remoteRuntime.getName()}"
   println "Uptime: ${remoteRuntime.getUptime()}"
   println "Classpath: ${remoteRuntime.getClassPath()}"
   println "Arguments: ${remoteRuntime.getInputArguments()}"
}


/**
 * Display operating system information based on MemoryMXBean associated with
 * the provided MBeanServerConnection.
 *
 * @param server MBeanServerConnection to use for obtaining memory information
 *    via the MemoryMXBean.
 */
def static void displayMemoryInfo(MBeanServerConnection server)
{
   def memoryMxBean = ManagementFactory.newPlatformMXBeanProxy(
                         server,
                         ManagementFactory.MEMORY_MXBEAN_NAME,
                         MemoryMXBean.class);
   println "HEAP Memory: ${memoryMxBean.getHeapMemoryUsage()}"
   println "Non-HEAP Memory: ${memoryMxBean.getNonHeapMemoryUsage()}"
}

The three Groovy code listings above together form a script that will use the Attach API to contact to an executing JVM without host or port specified and solely based on the provided process ID. The examples demonstrate use of several of the available MXBeans built into the virtual machine. Because it's Groovy, the code is somewhat more concise than its Java equivalent, especially because no checked exceptions must be explicitly handled and there is no need for explicit classes.

Much more could be done with the information provided via the Attach API and the MXBeans. For example, the Groovy script could be adjusted to persist some of the gathered details to build reports, Java mail could be used to alert individuals when memory constraints or other issues requiring notice occurred, and nearly anything else that can be done in Java could be added to these client scripts to make it easier to monitor and manage Java applications.

Running with the Attach API

The main implementation class of the Attach API, VirtualMachine, is located in the ${JAVA_HOME}\lib\tools.jar or %JAVA_HOME\lib\tools.jar JAR file included with the HotSpot SDK distribution. This file typically needs to be explicitly placed on the classpath of the Java client that uses the Attach API unless it is otherwise placed in a directory that is part of that inherent classpath. This is typically not required when using Groovy because it's normally already in Groovy's classpath. I briefly demonstrated this in the post Viewing Groovy Application's Classpath.

Conclusion

The Attach API makes it easier for the Java (or Groovy) developer to write clients that can communicate with, manage, and monitor Java processes. The Attach API provides the same benefits to the developer of custom JMX clients that JConsole and VisualVM leverage.

2 comments:

Chris said...

Dustin, thanks a lot. This will be useful for me but I am curious...does JMX provide for monitoring and management of anything other than threads, pools, or basic JVM/OS stats?

Specifically, I'm curious to see what hostnames/IP addresses are cached inside the JVM's name resolution cache. Is this possible?

@DustinMarx said...

Chris,

Unfortunately, I don't believe there is a JVM platform MXBean that supports this. The built-in JVM MXBeans are listed in Sun/Oracle's Overview of Monitoring and Management document in the Platform MBeans section. I had not seen this type of functionality before and did not see it after quickly perusing the list. I can see how having access to this type of information could be interesting and useful.

Dustin