How to retrieve JVM information using JRuby
By daniel on Apr 20, 2007
A few weeks ago, I posted a small example showing
how to programatically retrieve the JVM Management and Monitoring
information. Jeff Mesnil
also recently wrote a two parts blog article showing how to write a JMX client using JRuby.
Piecing these two articles together I'm going to show how to prototype this using JRuby in jconsole Script Shell plugin.
JConsole Script Shell Plugin
But what interest me today is to show how to expand on Jeff's article in order to show how you could prototype your ruby script to access the JVM Management and Monitoring information from within JConsole Script Shell tab.
Starting JConsole with a JRuby Script Shell tab
This is an easy step when you know how to do it and what pitfall to avoid. As it happens, the error message that you get back if things go wrong is a bit laconic, so it's better to know in advance what is suposed to work and what isn't.
First you will need to download several things (unless you already have them):
- A version of Java SE 6 JDK, or higher,
- The binary zip containing the various ScriptEngines offered by the java.net scripting project. Note that this zip does not contain the script language implementations, it only contains the ScriptEngine wrappers for each script.
- The implementation for JRuby, that you can get at http://jruby.codehaus.org/
Create a scripts folder somewhere and unzip the
jsr223-engines.zip you got from
inside. You will find the JRuby script engine in a
Then expand the jruby-bin-0.9.8 archive you got from
http://jruby.codehaus.org/ in that same scripts folder.
You will now find the jruby implementation jars in a
To start jconsole from the scripts folder use the following command:
# jconsole -J-Djava.class.path=<classpath> \\ # => classpath must contain tools.jar, jconsole.jar, # jruby-engine.jar, jruby jars... # -J-D<lang-property=language> \\ # => tells the demo which script language to instantiate # -pluginpath <pluginpath> # => pluginpath must contain the demo plugin # $JDK_HOME/bin/jconsole -J-Djava.class.path=$JDK_HOME/lib/jconsole.jar:$JDK_HOME/lib/tools.jar:jruby/build/jruby-engine.jar:jruby-0.9.8/lib/asm-2.2.3.jar:jruby-0.9.8/lib/jruby.jar \\ -J-Dcom.sun.demo.jconsole.console.language=jruby \\ -pluginpath $JDK_HOME/demo/scripting/jconsole-plugin/jconsole-plugin.jar
Be carefull not to include any empty element when specifying
the classpath - something like e.g.
If you do, the ScriptEngine instantiation is
likely to fail. In that case, JConsole will start, but when
you try to connect to the target VM, the Script Shell tab will
not be created. You will see a cryptic
error message on your terminal that says:
Exception in thread "AWT-EventQueue-0" java.lang.RuntimeException: cannot load jruby engine at com.sun.demo.scripting.jconsole.ScriptJConsolePlugin.createScriptEngine(ScriptJConsolePlugin.java:135)
When JConsole has started, connect to a target VM. After the connection is established, you should notice a Script Shell tab:
Click on the Script Shell tab and you'll be able to enter Ruby commands.
The MBeanServerConnection to the target VM can be obtained from the
$plugin global variable which is set by the Script Shell
rb>$plugin.context.MBeanServerConnection javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@18380a4 rb>
For instance, to count the MBeans in the target VM you just need to call:
rb>$plugin.context.MBeanServerConnection.MBeanCount 17 rb>
Or to get the list of MBeans registered in the target VM:
rb>$plugin.context.MBeanServerConnection::queryNames nil, nil [java.lang:type=Memory, java.lang:type=GarbageCollector,name=Copy, java.lang:type=MemoryPool,name=Code Cache, java.lang:type=Runtime, java.lang:type=ClassLoading, java.lang:type=Threading, java.util.logging:type=Logging, java.lang:type=Compilation, java.lang:type=MemoryPool,name=Eden Space, com.sun.management:type=HotSpotDiagnostic, java.lang:type=MemoryPool,name=Survivor Space, java.lang:type=GarbageCollector,name=MarkSweepCompact, java.lang:type=OperatingSystem, java.lang:type=MemoryPool,name=Tenured Gen, java.lang:type=MemoryPool,name=Perm Gen, JMImplementation:type=MBeanServerDelegate, java.lang:type=MemoryManager,name=CodeCacheManager] rb>
One of the pitfalls I immediately fell into was when I tried to save the connection to the target VM in a local variable. Each line of command that you type in is executed in its own local scope, and therefore, any local variable you try to set will be accessible only within that scope, i.e. within that line. So for instance:
rb>conn = $plugin.context.MBeanServerConnection ; conn.MBeanCount 17 rb>
works, but the following doesn't:
rb>conn = $plugin.context.MBeanServerConnection javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@94229e rb>conn.MBeanCount org.jruby.exceptions.RaiseException rb>
Here again, the error message you get when jruby raises an exception is a bit laconic ("org.jruby.exceptions.RaiseException") so it took me a while to figure out what was going wrong.
The bottom line is: if you want to be able to assign a variable on one line, and use it in the next, you must use a global variable assignation:
rb>$conn = $plugin.context.MBeanServerConnection javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@94229e rb>$conn.MBeanCount 17 rb>
Creating Proxies to access Platform MXBeans
So now we are ready to get back to Jeff's example:
rb>include_class 'java.lang.management.ManagementFactory' java.lang.management.ManagementFactory rb>include_class 'java.lang.management.MemoryMXBean' java.lang.management.MemoryMXBean rb>$conn = $plugin.context.MBeanServerConnection javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@94229e rb>$memory_mbean = ManagementFactory::newPlatformMXBeanProxy $conn, "java.lang:type=Memory", MemoryMXBean::java_class MXBeanProxy(javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@94229e[java.lang:type=Memory]) rb>$memory_mbean.heapMemoryUsage init = 0(0K) used = 22425448(21899K) committed = 31408128(30672K) max = 66387968(64832K) rb>$memory_mbean.nonHeapMemoryUsage init = 12746752(12448K) used = 28892200(28215K) committed = 29065216(28384K) max = 100663296(98304K) rb>
... and that's it! Nice, isn't it?
Update: I have also come across this nice article by Paul King, this time about using JMX with Groovy. Quoting:
Given that Groovy sits directly on top of Java, Groovy can leverage the tremendous amount of work already done for JMX with Java.
Update 2: I also came across this blog entry by Ivan who explains how to manage GlassFish using JRuby.