In Java 9, compiled code is no longer exclusively created using the built-in just-in-time (JIT) compilers. This blog post explores two ways that compiled code can be introduced without using a built-in JIT.
Java Virtual Machine Compiler Interface (JVM CI)
Java 9 specifies a new interface for JIT compilers that’s written in Java (JVMCI). This means that anybody can ship a JIT compiler that can be easily attached to a VM. The compiler can be the exclusive JIT compiler in a system, or it can work hand-in-hand with a built-in JIT compiler in tiered mode. In the first case, the compiler (which is just Java code, just like any application), interprets and eventually compiles itself. In the latter case however, the compiled code can be used for any compilation tier. Hotspot, like many other VMs, already defaults to a tiered compilation mode. In this mode, a method starts out by being interpreted, but after reaching a specific number of executions, it is compiled by the client compiler (which generates code quickly, but only with rather simple optimizations). Finally once enough executions have occurred, the code is compiled by the server compiler (which is comparably slow, but applies aggressive optimizations) for maximum performance. The custom JIT can be inserted at any level. This means it can be used both as a middle tier or as the final tier.
The following diagram shows how source code can be compiled using various JIT compilers.
Most application vendors will probably never develop and ship a custom JIT compiler. However, this interface is a necessary step towards supporting non-native JIT compilers. New compilers can be more easily extended, experimented with, and maintained by VM vendors.
Java Ahead-of-time Compiler (JAOTC)
The second new way of introducing compiled code into the JVM is by precompiling individual classes or entire modules into a native library (.SO or .DLL) (JAOTC). This library can then be passed to the JVM, which will bypass the interpretation step and use the compiled code right away.
To ship an application with ahead-of-time compiled libraries, you have to keep a few things in mind:
- The native library is not a replacement for a class file. The class file is still used to read metadata, and it is required for JIT compilations.
- As any compiled code is heavily influenced by the JVM configuration, the ahead-of-time compiler must be supplied with the same options that the executing JVM will use. If the actual JVM options differ, the native library will be ignored and the JVM will fall back to interpreting.
- The ahead-of-time compiled code will probably not be as efficient as the JIT-compiled code.
- As ahead-of-time compiled libraries are architecture dependent, one library for each supported architecture should be shipped along with the Java application. However, one should keep in mind that if anything goes wrong (for example, the arguments the library was compiled with don’t match, the architecture of the library doesn’t match, or an assumption the ahead-of-time compiler made turns out to be wrong), the VM will seamlessly fall back to interpreting the original bytecode.
Although the VM will start using the ahead-of-time compiled code right away (bypassing interpretation), code will still be eventually JIT compiled to achieve better performance. This feature is mainly designed to decrease startup time and boost performance in methods that are too rarely executed to be JIT compiled at all.
The following diagram shows how to precompile a simple class and how to hand it over to the JVM.
On a side note, the Oracle ahead-of-time compiler uses Graal as the backend, which in turn uses JVMCI to attach to the Hotspot VM.