Starting JVM on Mac with -XstartOnFirstThread programmatically

One of the nice features in LWJGL3 is that it comes with the SharedLibraryLoader, this automatically extracts any required natives.

This means you can create single executable fat jars without jumping too many hoops or having to fiddle with natives thus making distribution and running applications a lot easier. This works well on Windows and Linux as the user can just click a jar to start the application (or simply use ‘java -jar app.jar’ without having to type a long classpath or class name).

However one problem that I ran into was that on Mac you have to specify the “-XstartOnFirstThread” parameter and there is no easy solution to do this automatically with executable jars.

Therefore I’ve written a small method that basically checks if you are on Mac and if the XstartOnFirstThread option has been enabled, if not, it launches a new JVM and executes the same main method, settings, parameters and with the XstartOnFirstThread option enabled.

The above is difficult to get right and the code belows uses some nice tricks to detect everything, so should be useful for others who also want to use single file executable jars with LWJGL3 on Mac.


	public static boolean restartJVM() {
		
		String osName = System.getProperty("os.name");
		
		// if not a mac return false
		if (!osName.startsWith("Mac") && !osName.startsWith("Darwin")) {
			return false;
		}
		
		// get current jvm process pid
		String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
		// get environment variable on whether XstartOnFirstThread is enabled
		String env = System.getenv("JAVA_STARTED_ON_FIRST_THREAD_" + pid);
		
		// if environment variable is "1" then XstartOnFirstThread is enabled
		if (env != null && env.equals("1")) {
			return false;
		}
		
		// restart jvm with -XstartOnFirstThread
		String separator = System.getProperty("file.separator");
		String classpath = System.getProperty("java.class.path");
		String mainClass = System.getenv("JAVA_MAIN_CLASS_" + pid);
		String jvmPath = System.getProperty("java.home") + separator + "bin" + separator + "java";
		
		List<String> inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
		
		ArrayList<String> jvmArgs = new ArrayList<String>();
		
		jvmArgs.add(jvmPath);
		jvmArgs.add("-XstartOnFirstThread");
		jvmArgs.addAll(inputArguments);
		jvmArgs.add("-cp");
		jvmArgs.add(classpath);
		jvmArgs.add(mainClass);
		
		// if you don't need console output, just enable these two lines 
		// and delete bits after it. This JVM will then terminate.
		//ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs);
		//processBuilder.start();
		
		try {
			ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs);
			processBuilder.redirectErrorStream(true);
			Process process = processBuilder.start();
			
			InputStream is = process.getInputStream();
			InputStreamReader isr = new InputStreamReader(is);
			BufferedReader br = new BufferedReader(isr);
			String line;
			
			while ((line = br.readLine()) != null) {
				System.out.println(line);
			}
			
			process.waitFor();
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return true;
	}

To use the above method, you simply put the follow at the start of your main method

public static void main(String[] args) throws Exception {
	if (restartJVM()) {
		return;
	}
	// the rest of your main method
	}

The only downside is that as the new JVM runs in a new process you can’t get the console output in the same location without leaving the initial jvm running. If you don’t need the output in the same location the the original jvm can just terminate.

With the above you can run executable jars which use LWJGL3 on Mac by simply clicking on them.

2 Likes

That’s a great utility. Here is a slight modifications that takes the args into consideration too when rebooting an app.

package com.noblemaster.lib.boot.plaf.impl.commondesk.tool;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;

/**
 * Reboot utility for JVM.
 *
 * @author kappa
 */
public final class JVMReboot {

private JVMReboot() {
  // not used
}

/** Reboots the JVM on Mac OS X if we are not on the first thread! */
public static boolean restartJVM(String[] args) {
  String osName = System.getProperty("os.name");

  // if not a mac return false
  if ((!osName.startsWith("Mac")) && (!osName.startsWith("Darwin"))) {
    return false;
  }

  // get current jvm process pid
  String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
  // get environment variable on whether XstartOnFirstThread is enabled
  String env = System.getenv("JAVA_STARTED_ON_FIRST_THREAD_" + pid);

  // if environment variable is "1" then XstartOnFirstThread is enabled
  if ("1".equals(env)) {
    return false;
  }

  // restart jvm with -XstartOnFirstThread
  String separator = System.getProperty("file.separator");
  String classpath = System.getProperty("java.class.path");
  String mainClass = System.getenv("JAVA_MAIN_CLASS_" + pid);
  String jvmPath = System.getProperty("java.home") + separator + "bin" + separator + "java";

  ArrayList<String> jvmArgs = new ArrayList<String>(128);
  jvmArgs.add(jvmPath);
  jvmArgs.add("-XstartOnFirstThread");
  jvmArgs.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());  // <-- input arguments!
  jvmArgs.add("-cp");
  jvmArgs.add(classpath);
  jvmArgs.add(mainClass);
  for (int i = 0; i < args.length; i++) {
    jvmArgs.add(args[i]);
  }

  // if we want console output via same JVM
  final boolean consoleOutputViaSameJVM = false;
  try {
    if (consoleOutputViaSameJVM) {
      // with console output: the current JVM will continue & show console output...
      ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs);
      processBuilder.redirectErrorStream(true);
      Process process = processBuilder.start();

      InputStream is = process.getInputStream();
      InputStreamReader isr = new InputStreamReader(is);
      BufferedReader br = new BufferedReader(isr);
      String line;

      while ((line = br.readLine()) != null) {
        System.out.println(line);
      }

      process.waitFor();
    }
    else {
      // without console output: the current JVM will terminate!
      ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs);
      processBuilder.start();
    }
  }
  catch (Exception e) {
    e.printStackTrace();
  }

  return true;
  }
}
2 Likes

System.exit(process.waitFor());

1 Like