Episode 1: “The Evolution” — Java JIT Hotspot & C2 Compilers (Building “Super Optimum Java MicroServices Architecture" Series)"
By: A B Vijay Kumar
Tags: Microservices, Java, Graalvm, Containers, Kubernetes

Episode 1: “The Evolution”

Java JIT Hotspot & C2 compilers

With Java, we achieved the “write-once-run-anywhere” dream, in the early 90s. The approach was very simple. The Java programs are compiled to “byte-code”

Interesting fact: byte-code is called byte-code, as each instruction in byte-code is of byte length, so that it can be loaded into the CPU cache, and in fact there were also java CPUs built!!! didn’t take-off

We have JVM implementations, for each supported operating system. The respective JVM will “interpret” the byte-code to machine instruction (using something like a map). Obviously, this is slow, as the interpreter goes one statement at a time!!!

To speed up this, it makes sense to identify the code, that is run more commonly, and compile them ahead of time, and cache it 🤔.

That is exactly, what later versions of JVMs started doing. A performance counter was introduced, that counted the number of times a particular method/snippets of code is executed. Once a method/code snippet is used to a particular number of times (threshold), then that particular code snippet, is compiled, optimised & cached, by “C1 compiler”. Next time, that code snippet is called, it directly executes the compiled machine instructions from the cache, rather than going through the interpreter. This brought in the first level of optimisation.

While the code is getting executed, the JVM will perform runtime code profiling, and come up with code paths and hotspots. It then runs the “C2 compiler”, to further optimize the hot code paths…and hence the name “Hotspot”

C1 is faster, and good for short-running applications, while C2 is slower and heavy, but is ideal for long-running processes like daemons, servers etc, the code performs better over the time.

In Java 6, we have an option to use either C1 or C2 methods (with a command-line argument -client (for C1), -server (for C2)), in Java 7, we could use both, and from Java 8 onwards it became default behavior.

The below diagram illustrates the flow…

Here are some of the code optimization, that the JVM compiler

Removing null checks (for the variable that are never null)

Inlining smaller, most called methods (small methods) reducing the method calls

Optimizing the loops, by combining, unrolling & inversions

Removing the code that is never called (Dead code)

and many more…

Whatever said and done, JIT (Just-In-time compilation) is slow, as there is a lot of work that the JVM has to do in the runtime.

Ahead-of-Time compilation option was introduced since Java 9, where u can generate the final machine code, directly using jaotc

This code is compiled to a target architecture, so it is not portable…in X86, we can have both Java bytecode and AOT compiled code, working together.

The bytecode will go through the approach, that I explained previously (C1, C2) while the AOT compiled code directly goes and sits in the code cache, reducing the load on JVM. Typically the most frequently used libraries can be AOT compiled, for faster responses.

This is the story of Java VM…and pretty much every language has a similar story, where it goes thru the similar inception and over a period of time, the compiler/VM gets optimised to run faster

In the next episode, we will look at how GraalVM, takes this further, by reducing the footprint, optimising the execution and bring in support for polyglot/multi language interoperability.

The Holy Grail

You can read the blog here

Episode 2: GraalVM — “The Holy Grail”

👋 Join FAUN today and receive similar stories each week in your inbox! ️ Get your weekly dose of the must-read tech stories, news, and tutorials.

Follow us on Twitter 🐦 and Facebook 👥 and Instagram 📷 and join our Facebook and Linkedin Groups 💬