JIT Compiler
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
| Interpreter | JIT compiler | |
|---|---|---|
| When it works | Immediately, from the first instruction | After a method has run many times |
| How | Reads each bytecode instruction and carries it out | Translates a whole method into machine code for your processor |
| Speed of starting | Fast: no waiting | Compiling takes time and memory |
| Speed of running | Slow | Fast, 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:
- Interpreter. Every method starts here, while the JVM counts how often it runs.
- 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.
- 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
| Optimization | Idea |
|---|---|
| Inlining | Copy a small method's body into the caller, so there's no call at all |
| Dead code elimination | Remove code whose result is never used |
| Loop optimizations | Move work that doesn't change out of the loop; unroll short loops |
| Escape analysis | If an object never leaves a method, skip creating it on the heap |
| Branch prediction from profiles | Lay 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:
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);
}
}Total GST: 8942000000Run it with the -XX:+PrintCompilation option to see every method the JIT compiles. Here's the part about HotLoop from one run:
$ 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
- The first column is the time in milliseconds since the JVM started.
gstwas compiled within about 10 ms. - The third number is the tier:
3is C1 with profiling,4is C2. Sogstwent to C1 first, then C2. - made not entrant means the old C1 version was retired once the C2 version was ready.
- The
%marks on-stack replacement (OSR):mainruns only once, but its loop is hot, so the JVM compiles the loop and switches to the compiled version whilemainis 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:
$ time java -Xint HotLoop
Total GST: 8942000000
real 1.26
$ time java HotLoop
Total GST: 8942000000
real 0.10With 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
- Easy: In your own words, explain why the JVM doesn't JIT-compile every method as soon as the program starts.
- Harder: Run
HotLoopwith-XX:+PrintCompilationand find a line for a method from the Java library, such as one fromjava.lang.String. Why was a library method compiled when your code doesn't call it directly?
Key Takeaways
Was this tutorial helpful?