Calling jcmd Commands Programmatically

JCmd allows you to quickly get information on an existing JVM. This helpful for getting things like thread-dumps (see jstall for tool that heavily relies on this). In this blog post you’ll learn to send JCmd diagnostic commands programmatically. You can find the whole code of this blog post on GitHub.

Let’s get a sample application running:

> java Loop.java &
[1] 23462

Now we want to obtain the VM arguments and the Java command, that’s easy on the command line via jcmd:

> jcmd 23462 VM.command_line
23462:
VM Arguments:
jvm_args: --add-modules=ALL-DEFAULT 
java_command: jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher Loop.java
java_class_path (initial): .
Launcher Type: SUN_STANDARD

But how can we do it programmatically? Calling jcmd always starts a new JVM, which we might not want.

This is where the Diagnostic MBean comes into play.

But first how does all this work in the observed JVM? This is all based on the Java Management Extensions (JMX) technology, the built-in management tools of the JVM. This is essentially a native agent that runs alongside your application and opens a port that other tools like jcmd can talk. The agent can be configured using system properties, to e.g. set the port or password.

The Java runtime library provides us with methods to connect to a JVM properly and then work with the JMX agent.

First, we attach to the JVM and obtain a VirtualMachine mirror object (please be aware that the JVM also contains another VirtualMachine class for the debugger, so don’t confuse the two):

var vm = VirtualMachine.attach(String.valueOf(pid));

We can use the VM object to e.g. attach an agent or get the system properties. But what
is important in our context: We can also start the JMX agent (with and without custom properties)
using the startLocalManagementAgent method:

var serviceUrl = new JMXServiceURL(vm.startLocalManagementAgent());

This method returns a string that represents the local JVM Service URL that has the following format:

service:jmx:protocol:sap

Which in our example looks something like:

service:jmx:rmi://127.0.0.1/stub/rO...g=

The base64 string is a remote method invocation stub that contains a serialized RMI endpoint. When we decode the stub, it looks something like:

... sr.javax.management.remote.rmi.RMIServerImpl_Stubxrjava.rmi.server.RemoteStub���ɋ�exrjava.rmi.server.RemoteObject ...

Essentially encoding how to access to connect to the JMX RMIServer.

Anyway: Now we connect to this end point via the JMXConnectorFactory
and the JMXConnector to get a proper MBeanServerConnection:

var connector = JMXConnectorFactory.connect(serviceUrl);
var mbeanServer = connector.getMBeanServerConnection();
var diagnosticCommand =  new ObjectName("com.sun.management:type=DiagnosticCommand");

This allows to interact with JMX and diagnostic command. We’re almost there.

In principal we can invoke the individual JCmd commands via the MBeanServerConnection:

Object invoke(ObjectName name,
 String operationName,
 Object[] params,
 String[] signature)

The only problem is that the operation name is not the name used with jcmd (and defined in the code for every command class):

String[] cmdArgs = new String[] { }; // currently no command arguments passed
Object[] params = new Object[] { cmdArgs };
String[] signature = new String[] { "[Ljava.lang.String;" };
var res = mbeanServer.invoke(diagnosticCommand, "vmCommandLine", params, signature);
System.out.println(res);

But a name that adheres to the Java method name guidelines, as it’s a MBean method name:
From VM.command_line to vmCommandLine. This transformation is implemented in the JMXExecutor and in the DiagnosticCommandImpl (the JVM handles the MBean names by iterating over all jcmd command and comparing the transformed name to the name incoming from the JMXExecutor). With two different transformation implementations.

But because the OpenJDK is GPLv2 licensed, I had to create my own
MIT licensed version:

private static String transformJcmdToMBeanName(String cmd) {
    StringBuilder out = new StringBuilder();
    boolean inFirstSegment = true;
    boolean capitalizeNext = false;
    
    for (int i = 0; i < cmd.length(); i++) {
        char c = cmd.charAt(i);
        if (c == '.' || c == '_') {
            // separators are removed and next character is capitalized
            inFirstSegment = false;
            capitalizeNext = true;
            continue;
        }
    
        if (capitalizeNext) {
            out.append(Character.toUpperCase(c));
            capitalizeNext = false;
        } else if (inFirstSegment) {
            out.append(Character.toLowerCase(c));
        } else {
            out.append(c);
        }
    }
    
    return out.toString();
}

The overall code to call jcmd commands programmatically is then:

public static void main(String[] args) throws Exception {
    int pid = Integer.parseInt(args[0]);
    String cmd = args[1];

    var vm = VirtualMachine.attach(String.valueOf(pid));
    var serviceUrl = new JMXServiceURL(vm.startLocalManagementAgent());

    var connector = JMXConnectorFactory.connect(serviceUrl);
    var mbeanServer = connector.getMBeanServerConnection();
    var diagnosticCommand =  new ObjectName("com.sun.management:type=DiagnosticCommand");
    String[] cmdArgs = new String[] { }; // currently no command arguments supported
    Object[] params = new Object[] { cmdArgs };
    String[] signature = new String[] { "[Ljava.lang.String;" };
    var res = mbeanServer.invoke(diagnosticCommand, transformJcmdToMBeanName(cmd), params, signature);
    System.out.println(res);
}

That’s all. It took me some time to figure it out, therfore this blog post. I hope it’s helpful to you. See you in the week or two on something bytecode related.

This blog post is part of my work in the SapMachine team at SAP, making profiling easier for everyone.

Author

  • Johannes Bechberger

    Johannes Bechberger is a JVM developer working on profilers and their underlying technology in the SapMachine team at SAP. This includes improvements to async-profiler and its ecosystem, a website to view the different JFR event types, and improvements to the FirefoxProfiler, making it usable in the Java world. His work today comprises many open-source contributions and his blog, where he regularly writes on in-depth profiling and debugging topics. He also works on hello-ebpf, the first eBPF library for Java. His most recent contribution is the new CPU Time Profiler in JDK 25.

    View all posts

New posts like these come out at least every two weeks, to get notified about new posts, follow me on BlueSky, Twitter, Mastodon, or LinkedIn, or join the newsletter:

Leave a Reply

Your email address will not be published. Required fields are marked *