JIT Compiler

Naveen AnamaneniUpdated · 6 min read

On your first trip to a new office, you check the map at every turn. By the third week, you drive there without thinking, and you've found a shortcut. The JVM learns your program in a similar way. It starts by interpreting bytecode carefully, one instruction at a time. As it notices which methods run again and again, the JIT (just-in-time) compiler turns them into fast machine code, and then keeps improving them.

Interpreter vs JIT Compiler

InterpreterJIT compiler
When it worksImmediately, from the first instructionAfter a method has run many times
HowReads each bytecode instruction and carries it outTranslates a whole method into machine code for your processor
Speed of startingFast: no waitingCompiling takes time and memory
Speed of runningSlowFast, often close to C++

The JVM uses both. Code that runs once, like start-up code, is just interpreted: compiling it would cost more than it saves. Code that runs millions of times, like the body of a busy loop, gets compiled.

Tiered Compilation: C1, Then C2

HotSpot actually has two JIT compilers, and uses them in stages, called tiered compilation:

A method moves from the interpreter to C1 to C2 as it gets hot
  1. Interpreter. Every method starts here, while the JVM counts how often it runs.
  2. C1, the quick compiler. Once a method gets warm (by default around 200 calls in HotSpot), C1 compiles it fast, with simple optimizations, and adds code to profile it: which branches are taken, which types appear.
  3. C2, the optimizing compiler. When the method gets really hot (around 5,000 calls by default), C2 compiles it again, using the profile to make bold optimizations.

If one of C2's guesses turns out wrong later (for example, a type it never saw appears), the JVM quietly deoptimizes: it throws away that machine code, goes back to the interpreter, and may compile again with better information. Your program keeps running correctly the whole time.

What C2 Does to Make Code Fast

OptimizationIdea
InliningCopy a small method's body into the caller, so there's no call at all
Dead code eliminationRemove code whose result is never used
Loop optimizationsMove work that doesn't change out of the loop; unroll short loops
Escape analysisIf an object never leaves a method, skip creating it on the heap
Branch prediction from profilesLay out code so the common path runs straight through

You don't write any of this. You write clear Java; the JIT compiler does the tuning.

Watch the JIT at Work

This program calls a small method a hundred million times:

HotLoop.java▶ Try it
public class HotLoop {
    static int gst(int price) {
        return price * 18 / 100;
    }
 
    public static void main(String[] args) {
        long total = 0;
        for (int i = 1; i <= 100_000_000; i++) {
            total += gst(i % 1000);
        }
        System.out.println("Total GST: " + total);
    }
}
Output
Total GST: 8942000000

Run it with the -XX:+PrintCompilation option to see every method the JIT compiles. Here's the part about HotLoop from one run:

Terminal
$ java -XX:+PrintCompilation HotLoop | grep HotLoop
10    8       3       HotLoop::gst (8 bytes)
10    9       4       HotLoop::gst (8 bytes)
10    8       3       HotLoop::gst (8 bytes)   made not entrant
11   10 %     3       HotLoop::main @ 4 (41 bytes)
11   12 %     4       HotLoop::main @ 4 (41 bytes)

How to read it

  1. The first column is the time in milliseconds since the JVM started. gst was compiled within about 10 ms.
  2. The third number is the tier: 3 is C1 with profiling, 4 is C2. So gst went to C1 first, then C2.
  3. made not entrant means the old C1 version was retired once the C2 version was ready.
  4. The % marks on-stack replacement (OSR): main runs only once, but its loop is hot, so the JVM compiles the loop and switches to the compiled version while main is still running.

Now compare running with the JIT turned off (-Xint means "interpreter only"). These times are from one laptop; yours will differ, but the gap will be similar:

Terminal
$ time java -Xint HotLoop
Total GST: 8942000000
real 1.26
 
$ time java HotLoop
Total GST: 8942000000
real 0.10

With the JIT, the same program ran about 12 times faster.

Try it yourself: change the loop limit to 1_000 and run both versions again. Is the JIT still worth it for such a short run?

Common Mistakes

Thinking Java is "interpreted, so slow". Only cold code is interpreted. Hot code runs as optimized machine code.

Measuring speed on the first run. The first calls are interpreted and include compiling time. Measure after warm-up.

Trying to "help" the JIT with tricks. Manual micro-optimizations often make code harder to read and no faster, because the JIT already does them. Write clear code first, then measure.

Practice

  1. Easy: In your own words, explain why the JVM doesn't JIT-compile every method as soon as the program starts.
  2. Harder: Run HotLoop with -XX:+PrintCompilation and find a line for a method from the Java library, such as one from java.lang.String. Why was a library method compiled when your code doesn't call it directly?

Quick check: JIT compiler

4 questions
1What does the JIT compiler do?
2In HotSpot's tiered compilation, which compiler makes the most optimized code?
3What does a % in -XX:+PrintCompilation output mean?
4Why should you warm up the JVM before timing code?

Key Takeaways

Was this tutorial helpful?

Lesson 26 of 482