Is *this* really the best way to start a second JVM from Java code?

Viewed 24743

This is a followup to my own previous question and I'm kind of embarassed to ask this... But anyway: how would you start a second JVM from a standalone Java program in a system-independent way? And without relying on for instance an env variable like JAVA_HOME as that might point to a different JRE than the one that is currently running. I came up with the following code which actually works but feels just a little awkward:

public static void startSecondJVM() throws Exception {
    String separator = System.getProperty("file.separator");
    String classpath = System.getProperty("java.class.path");
    String path = System.getProperty("java.home")
                + separator + "bin" + separator + "java";
    ProcessBuilder processBuilder = 
                new ProcessBuilder(path, "-cp", 
                classpath, 
                AnotherClassWithMainMethod.class.getName());
    Process process = processBuilder.start();
    process.waitFor();
}

Also, the currently running JVM might have been started with some other parameters (-D, -X..., ...) that the second JVM would not know about.

4 Answers

I think that the answer is "Yes". This probably as good as you can do in Java using system independent code. But be aware that even this is only relatively system independent. For example, in some systems:

  1. the JAVA_HOME variable may not have been set,
  2. the command name used to launch a JVM might be different (e.g. if it is not a Sun JVM), or
  3. the command line options might be different (e.g. if it is not a Sun JVM).

If I was aiming for maximum portability in launching a (second) JVM, I think I would do it using wrapper scripts.

It's not clear to me that you would always want to use exactly the same parameters, classpath or whatever (especially -X kind of stuff - for example, why would the child need the same heap settings as its parents) when starting a secondary process.

I would prefer to use an external configuration of some sort to define these properties for the children. It's a bit more work, but I think in the end you will need the flexibility.

To see the extent of possible configuration settings you might look at thye "Run Configurations" settings in Eclipse. Quite a few tabs worth of configuration there.

Here's a way that determines the java executable which runs the current JVM using ProcessHandle.current().info().command().

The ProcessHandle API also should allow to get the arguments. This code uses them for the new JVM if available, only replacing the current class name with another sample class. (Finding the current main class inside the arguments gets harder if you don't know its name, but in this demo it's simply "this" class. And maybe you want to reuse the same JVM options or some of them, but not the program arguments.)

However, for me (openjdk version 11.0.2, Windows 10), the ProcessInfo.arguments() is empty, so the fallback else path gets executed.

package test;
import java.lang.ProcessBuilder.Redirect;
import java.lang.management.ManagementFactory;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TestStartJvm {
    public static void main(String[] args) throws Exception {
        ProcessHandle.Info currentProcessInfo = ProcessHandle.current().info();
        List<String> newProcessCommandLine = new LinkedList<>();
        newProcessCommandLine.add(currentProcessInfo.command().get());

        Optional<String[]> currentProcessArgs = currentProcessInfo.arguments();
        if (currentProcessArgs.isPresent()) { // I know about orElse, but sometimes isPresent + get is handy
            for (String arg: currentProcessArgs.get()) {
                newProcessCommandLine.add(TestStartJvm.class.getName().equals(arg) ? TargetMain.class.getName() : arg);
            }
        } else {
            System.err.println("don't know all process arguments, falling back to passed args array");
            newProcessCommandLine.add("-classpath");
            newProcessCommandLine.add(ManagementFactory.getRuntimeMXBean().getClassPath());
            newProcessCommandLine.add(TargetMain.class.getName());
            newProcessCommandLine.addAll(List.of(args));
        }

        ProcessBuilder newProcessBuilder = new ProcessBuilder(newProcessCommandLine).redirectOutput(Redirect.INHERIT)
                .redirectError(Redirect.INHERIT);
        Process newProcess = newProcessBuilder.start();
        System.out.format("%s: process %s started%n", TestStartJvm.class.getName(), newProcessBuilder.command());
        System.out.format("process exited with status %s%n", newProcess.waitFor());
    }

    static class TargetMain {
        public static void main(String[] args) {
            System.out.format("in %s: PID %s, args: %s%n", TargetMain.class.getName(), ProcessHandle.current().pid(),
                    Stream.of(args).collect(Collectors.joining(", ")));
        }
    }
}

Before ProcessHandle was added in Java 9, I did something like this to query the current JVM's command-line:

  • Let the user pass or configure a "PID to command-line" command template; under Windows, this could be wmic process where 'processid=%s' get commandline /format:list.
  • Determine PID using java.lang.management.ManagementFactory.getRuntimeMXBean().getPid().
  • Expand command template; execute; parse its output.
Related