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
  1. #!/usr/bin/env groovy  
  2. // getJvmDetails.groovy  
  3. //  
  4. // Main script for extracting JVM details via Attach API and JMX.  
  5. // Accepts single parameter which is the process ID (pid) of the Java application  
  6. // whose JVM is to be connected to.  
  7. //  
  8. import static attachToVirtualMachine.retrieveConnector  
  9. import static displayMxBeanDerivedInfo.*  
  10.   
  11. def serverConnection = attachToVirtualMachine.retrieveServerConnection(args[0])  
  12.   
  13. displayMxBeanDerivedInfo.displayThreadInfo(serverConnection)  
  14. displayMxBeanDerivedInfo.displayOperatingSystemInfo(serverConnection)  
  15. displayMxBeanDerivedInfo.displayRuntimeInfo(serverConnection)  
  16. displayMxBeanDerivedInfo.displayMemoryInfo(serverConnection)  

attachToVirtualMachine.groovy
  1. // attachToVirtualMachine.groovy  
  2. //  
  3. // Provide an MBeanServerConnection acquired via the Attach API.  
  4.   
  5. import javax.management.MBeanServerConnection  
  6. import javax.management.remote.JMXConnector  
  7. import javax.management.remote.JMXConnectorFactory  
  8. import javax.management.remote.JMXServiceURL  
  9.   
  10. import com.sun.tools.attach.VirtualMachine  
  11.   
  12.   
  13. /** 
  14.  * Provide an MBeanServerConnection based on the provided process ID (pid). 
  15.  * 
  16.  * @param pid Process ID of Java process for which MBeanServerConnection is 
  17.  *    desired. 
  18.  * @return MBeanServerConnection connecting to Java process identified by pid. 
  19.  */  
  20. def static MBeanServerConnection retrieveServerConnection(String pid)  
  21. {  
  22.    println "Get JMX Connector for pid ${pid}!"  
  23.    def connectorAddressStr = "com.sun.management.jmxremote.localConnectorAddress"  
  24.    def jmxUrl = retrieveUrlForPid(pid, connectorAddressStr)  
  25.    def jmxConnector = JMXConnectorFactory.connect(jmxUrl)  
  26.    return jmxConnector.getMBeanServerConnection()  
  27. }  
  28.   
  29.   
  30. /** 
  31.  * Provide JMX URL for attaching to the provided process ID (pid). 
  32.  * 
  33.  * @param @pid Process ID for which JMX URL is needed to connect. 
  34.  * @param @connectorAddressStr String for connecting. 
  35.  * @return JMX URL to communicating with Java process identified by pid. 
  36.  */  
  37. def static JMXServiceURL retrieveUrlForPid(String pid, String connectorAddressStr)  
  38. {  
  39.    // Attach to the target application's virtual machine  
  40.    def vm = VirtualMachine.attach(pid)  
  41.   
  42.    // Obtain Connector Address  
  43.    def connectorAddress =  
  44.       vm.getAgentProperties().getProperty(connectorAddressStr)  
  45.   
  46.    // Load Agent if no connector address is available  
  47.    if (connectorAddress == null)  
  48.    {  
  49.       def agent = vm.getSystemProperties().getProperty("java.home") +  
  50.           File.separator + "lib" + File.separator + "management-agent.jar"  
  51.       vm.loadAgent(agent)  
  52.   
  53.       // agent is started, get the connector address  
  54.       connectorAddress =  
  55.          vm.getAgentProperties().getProperty(connectorAddressStr)  
  56.    }  
  57.   
  58.    return new JMXServiceURL(connectorAddress);  
  59. }  

displayMxBeanDerivedInfo.groovy
  1. // displayMxBeanDerivedInfo.groovy  
  2. //  
  3. // Display details regarding attached virtual machine and associated MXBeans.  
  4.   
  5. import java.lang.management.ManagementFactory  
  6. import java.lang.management.MemoryMXBean  
  7. import java.lang.management.OperatingSystemMXBean  
  8. import java.lang.management.RuntimeMXBean  
  9. import java.lang.management.ThreadMXBean  
  10. import javax.management.MBeanServerConnection  
  11.   
  12. /** 
  13.  * Display thread information based on ThreadMXBean associated with the provided 
  14.  * MBeanServerConnection. 
  15.  * 
  16.  * @param server MBeanServerConnection to use for obtaining thread information 
  17.  *    via the ThreadMXBean. 
  18.  */  
  19. def static void displayThreadInfo(MBeanServerConnection server)  
  20. {  
  21.    def remoteThreadBean = ManagementFactory.newPlatformMXBeanProxy(  
  22.                              server,  
  23.                              ManagementFactory.THREAD_MXBEAN_NAME,  
  24.                              ThreadMXBean.class);  
  25.   
  26.    println "Deadlocked Threads: ${remoteThreadBean.findDeadlockedThreads()}"  
  27.    println "Monitor Deadlocked Threads: ${remoteThreadBean.findMonitorDeadlockedThreads()}"  
  28.    println "Thread IDs: ${Arrays.toString(remoteThreadBean.getAllThreadIds())}"  
  29.    def threads = remoteThreadBean.dumpAllThreads(truetrue);  
  30.    threads.each  
  31.    {  
  32.       println "\t${it.getThreadName()} (${it.getThreadId()}): ${it.getThreadState()}"  
  33.    }  
  34. }  
  35.   
  36.   
  37. /** 
  38.  * Display operating system information based on OperatingSystemMXBean associated 
  39.  * with the provided MBeanServerConnection. 
  40.  * 
  41.  * @param server MBeanServerConnection to use for obtaining operating system 
  42.  *    information via the OperatingSystemMXBean. 
  43.  */  
  44. def static void displayOperatingSystemInfo(MBeanServerConnection server)  
  45. {  
  46.    def osMxBean = ManagementFactory.newPlatformMXBeanProxy(  
  47.                      server,  
  48.                      ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME,  
  49.                      OperatingSystemMXBean.class)  
  50.    println "Architecture: ${osMxBean.getArch()}"  
  51.    println "Number of Processors: ${osMxBean.getAvailableProcessors()}"  
  52.    println "Name: ${osMxBean.getName()}"  
  53.    println "Version: ${osMxBean.getVersion()}"  
  54.    println "System Load Average: ${osMxBean.getSystemLoadAverage()}"  
  55. }  
  56.   
  57.   
  58. /** 
  59.  * Display operating system information based on RuntimeMXBean associated with 
  60.  * the provided MBeanServerConnection. 
  61.  * 
  62.  * @param server MBeanServerConnection to use for obtaining runtime information 
  63.  *    via the RuntimeMXBean. 
  64.  */  
  65. def static void displayRuntimeInfo(MBeanServerConnection server)  
  66. {  
  67.    def remoteRuntime = ManagementFactory.newPlatformMXBeanProxy(  
  68.                           server,  
  69.                           ManagementFactory.RUNTIME_MXBEAN_NAME,  
  70.                           RuntimeMXBean.class);  
  71.   
  72.    println "Target Virtual Machine: ${remoteRuntime.getName()}"  
  73.    println "Uptime: ${remoteRuntime.getUptime()}"  
  74.    println "Classpath: ${remoteRuntime.getClassPath()}"  
  75.    println "Arguments: ${remoteRuntime.getInputArguments()}"  
  76. }  
  77.   
  78.   
  79. /** 
  80.  * Display operating system information based on MemoryMXBean associated with 
  81.  * the provided MBeanServerConnection. 
  82.  * 
  83.  * @param server MBeanServerConnection to use for obtaining memory information 
  84.  *    via the MemoryMXBean. 
  85.  */  
  86. def static void displayMemoryInfo(MBeanServerConnection server)  
  87. {  
  88.    def memoryMxBean = ManagementFactory.newPlatformMXBeanProxy(  
  89.                          server,  
  90.                          ManagementFactory.MEMORY_MXBEAN_NAME,  
  91.                          MemoryMXBean.class);  
  92.    println "HEAP Memory: ${memoryMxBean.getHeapMemoryUsage()}"  
  93.    println "Non-HEAP Memory: ${memoryMxBean.getNonHeapMemoryUsage()}"  
  94. }  

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