42

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.

Community
  • 1
  • 1
Robert Petermeier
  • 4,122
  • 4
  • 29
  • 37
  • Look like the code was taken from this answer http://stackoverflow.com/a/723914/3520484. – Mark Sep 24 '16 at 16:24
  • @Mark: No, it wasn't. I wrote this question more than ten years ago, but I still remember that I adapted real code from a project. I changed the hard-coded "/" to `System.getProperty("file.separator")`, not knowing about `File.separator`, which was used in the answer you linked to. – Robert Petermeier Nov 15 '19 at 20:51

4 Answers4

6

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.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • 1
    Thanks for that. The JAVA_HOME variable is not relevant here as System.getProperty("java.home") != JAVA_HOME. Do you have an example of a JVM where my code would not work because the executable has a different name? – Robert Petermeier Aug 05 '09 at 18:09
  • @Robert: a couple of examples: IBM (and others?) have "javaw" as an alternative, and Jikes RVM is launched using "rvm". (And for JNode, you could run "java" or "org.jnode.command.common.java" ... commands are not identified by pathnames.) – Stephen C Aug 05 '09 at 21:26
5

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.

djna
  • 54,992
  • 14
  • 74
  • 117
  • A config file for the JVM is a good idea. I think it should be editable in a dialog so that users can but need not tweak the parameters (much like the Eclipse example). Thanks for that. What I was also aiming at was the path to the executable. As Stephen C mentioned the code will not work if the executable of the JVM is not called "java". Is there any system-independent way to find the executable name? – Robert Petermeier Aug 05 '09 at 18:06
  • As it happens I'm currently working with a java whose executable is not "java" by name. And I'm spawning from with a traditional Java. And I have at least 4 different java executables onmy machine, and to "not java" javas. You can't deduce the reliably, so why not include the path to the java as one of the configurable items. You will see that this is actually what Eclipse does, albeit in a not very easy to configure way. – djna Aug 05 '09 at 21:29
  • 1
    Why is this the accepted answer? if it doesn't answer the question that is waiting a "yes" or "no", and in the later situation I suppose an alternative for the "no". – Jaime Hablutzel Mar 18 '14 at 04:55
5

To find the java executable that your code is currently running under (i.e. the 'path' variable in your question's sample code) there is a utility method within apache ant that can help you. You don't have to build your code with ant - just use it as a library, for this one method.

It is:

org.apache.tools.ant.util.JavaEnvUtils.getJreExecutable("java")

It takes care of the sort of special cases with different JVM vendors that others have mentioned. (And looking at the source code for it, there are more special cases than I would have imagined.)

It's in ant.jar. ant is distributed under the Apache license so hopefully you can use it how you want without hassle.

Daniel Tripp
  • 111
  • 1
  • 3
0

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.
EndlosSchleife
  • 515
  • 7
  • 19