Java can also be executed directly without compiling?

Keywords: Java JDK jvm Unix

We all know that java is a static language, that is to say, if you want to execute a java program, you must compile it first and then execute it.

So why does this article say that java can be executed without compiling?

Actually, this is a new feature added in OpenJDK11 to enable the java source code of a single file to be executed directly without compiling.

This feature is described in detail in the following JEP:

http://openjdk.java.net/jeps/330

Let's start with a small experiment:

$ cat Test.java
public class Test {
  public static void main(String[] args) {
    System.out.println("hello");
  }
}
$ java Test.java
hello

It can really be executed. It's amazing.

JEP 330 also mentions that under Unix-like operating systems, the above code can also be executed in the form of "Shebang".

Let's write another example to see:

$ cat Test
#!/usr/bin/java --source 12
public class Test {
  public static void main(String[] args) {
    System.out.println("hello");
  }
}
$ chmod +x Test
$ ./Test
hello

See, the code we write in java can be executed directly like a shell script.

So how does all this work in JVM? Why can static languages be executed dynamically like scripts?

Let's look at the corresponding JVM source code:

// src/java.base/share/native/libjli/java.c
static jboolean
ParseArguments(int *pargc, char ***pargv,
               int *pmode, char **pwhat,
               int *pret, const char *jrepath)
{
    ...
    if (mode == LM_SOURCE) {
        ...
        *pwhat = SOURCE_LAUNCHER_MAIN_ENTRY;
        ...
    }
    ...
    *pmode = mode;
    return JNI_TRUE;
}

When the Java program we want to execute is a java source file, the mode in this method is set to LM_SOURCE.

The pwhat pointer points to the java class with the main method that we ultimately want to execute. As we can see from the above, when mode is LM_SOURCE, the java class that we ultimately execute is not the java class corresponding to the java source file we provide, but the java class defined by the macro SOURCE_LAUNCHER_MAIN_ENTRY.

Let's see what the java class for this macro is:

// src/java.base/share/native/libjli/java.c
#define SOURCE_LAUNCHER_MAIN_ENTRY "jdk.compiler/com.sun.tools.javac.launcher.Main"

As can be seen from the above, it is a class in the jdk.compiler module, and the main method in which the java command is ultimately executed is the main method in this class.

What are the parameters of this main method?

Actually, it's the java source file that we provide, but for clarity, we verify it by the following way:

$ _JAVA_LAUNCHER_DEBUG=1 java Test.java
----_JAVA_LAUNCHER_DEBUG----
# Eliminate irrelevant information
Source is 'jdk.compiler/com.sun.tools.javac.launcher.Main'
App's argc is 1
    argv[ 0] = 'Test.java'
# Eliminate irrelevant information
----_JAVA_LAUNCHER_DEBUG----
hello

If we set up the environment variable _JAVA_LAUNCHER_DEBUG before starting java, some run-time data will be output inside the JVM for debugging. For example, from the output above, we can see that the Java class with main method to be executed by the Java command is jdk. compiler/com. sun. tools. javac. launcher. Main, whose reference is jdk.compiler/com.sun.tools.javac.launcher.Main. The number is Test.java, which is exactly the same as what we analyzed above.

That is to say, when we execute java commands in the form of source files, the main method we call is the main method in jdk.compiler/com.sun.tools.javac.launcher.Main, whose parameters are the java source files we want to execute.

Let's see how this main method actually executes our source files:

// com.sun.tools.javac.launcher.Main
public class Main {
    ...
    public static void main(String... args) throws Throwable {
        try {
            new Main(System.err).run(VM.getRuntimeArguments(), args);
        } catch (Fault f) {
            ...
        }
    }
    ...
    public void run(String[] runtimeArgs, String[] args) throws Fault, InvocationTargetException {
        Path file = getFile(args); // Source files to be executed
        ...
        String mainClassName = compile(file, getJavacOpts(runtimeArgs), context);

        String[] appArgs = Arrays.copyOfRange(args, 1, args.length);
        execute(mainClassName, appArgs, context);
    }
    ...
    private void execute(String mainClassName, String[] appArgs, Context context)
            throws Fault, InvocationTargetException {
        ...
        try {
            Class<?> appClass = Class.forName(mainClassName, true, cl);
            Method main = appClass.getDeclaredMethod("main", String[].class);
            ...
            main.invoke(0, (Object) appArgs);
        } catch (ClassNotFoundException e) {
            ...
        }
    }
}

Here we just list the general logic of the method, but it's enough to see how it works.

The source code we want to execute is compiled by java's compiler, and then the main method is called to continue executing the logic we write.

It turned out to be so simple.

However, the dynamic execution of java source code has left us a lot of imagination space, although its implementation mechanism is very rough, but it is still friendly to users.

I hope this article can bring you some harvest.

Finish.

For more original articles, please pay attention to my Wechat Public Number:

Posted by jcantrell on Sun, 11 Aug 2019 06:19:30 -0700