Let's get a sample application running:
> java Loop.java &
[1] 12847Now 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_STANDARDBut how can we do it programmatically? Calling jcmd always starts
a new JVM which we might not want.
This where the ... comes to our help.
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 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 implementation.
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());
System.out.println(serviceUrl);
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);
}MIT