Java也可以不用编译直接执行了

2次阅读

共计 2762 个字符,预计需要花费 7 分钟才能阅读完成。

我们都知道 java 是静态语言,也就是说,如果你想执行 java 程序,就必须先编译,再执行。

那本文为什么说,java 可以不编译直接执行了呢?

其实,这个是 OpenJDK11 里新加的一个 feature,目的是使单个文件的 java 源码可以无需编译,直接执行。

下面的 JEP 里对该特性做了详细的描述:

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

我们先写个小例子实验下:

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

真的可以执行,神奇。

JEP 330 中还提到,在类 Unix 操作系统下,上面的代码还可以以 “Shebang” 形式执行。

我们再写一个例子看下:

$ 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

看到没,我们用 java 写的代码居然可以像 shell 脚本一样直接执行了。

那这一切在 JVM 中又是怎么实现的呢?静态语言为什么也可以像脚本一样动态执行了呢?

下面我们来看下对应的 JVM 源码:

// 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;
}

当我们要执行的 java 程序是 java 源文件时,该方法中的 mode 就会被设置为 LM_SOURCE。

pwhat 指针指向的是我们最终要执行的带 main 方法的 java 类,由上我们可以看到,在 mode 为 LM_SOURCE 时,最终执行的 java 类并不是我们提供的 java 源文件对应的 java 类,而是 SOURCE_LAUNCHER_MAIN_ENTRY 宏定义的 java 类。

我们看下这个宏对应的 java 类是什么:

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

由上可见,它是 jdk.compiler 模块里的一个类,java 命令最终执行的 main 方法就是这个类里的 main 方法。

那这个 main 方法的参数是什么呢?

其实就是我们提供的 java 源文件,不过为了更加明确,我们还是通过以下方式验证下:

$ _JAVA_LAUNCHER_DEBUG=1 java Test.java
----_JAVA_LAUNCHER_DEBUG----
# 省略无关信息
Source is 'jdk.compiler/com.sun.tools.javac.launcher.Main'
App's argc is 1
    argv[0] = 'Test.java'
# 省略无关信息
----_JAVA_LAUNCHER_DEBUG----
hello

如果我们在启动 java 之前,设置了_JAVA_LAUNCHER_DEBUG 环境变量,JVM 内部就会输出一些运行时的数据来供我们调试,比如,由上面的输出我们可以看到,java 命令将要执行的带 main 方法的 java 类为 jdk.compiler/com.sun.tools.javac.launcher.Main,其参数为 Test.java,正好和我们上文中分析的是一样的。

也就是说,当我们以源文件形式执行 java 命令时,最终调用的 main 方法是 jdk.compiler/com.sun.tools.javac.launcher.Main 里的 main 方法,其参数为我们要执行的 java 源文件。

下面我们再来看下这个 main 方法究竟是如何执行我们的源文件的:

// 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); // 我们要执行的源文件
        ...
        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) {...}
    }
}

在这里我们只列出了相关方法的大致逻辑,不过已经足够能看出,它到底是怎么执行的了。

我们要执行的源码先被 java 的 compiler 编译,然后又调用了其 main 方法继续执行我们写的逻辑。

原来是如此简单。

不过,java 源码可动态执行的特性还是给我们留下了很多想像空间,虽然其实现机制很粗暴,但对用户来说还算是友好的。

希望本篇文章能给各位同学带来一些收获。

完。

更多原创文章,请关注我微信公众号:

正文完
 0